Merge branch 'develop' of github.com:Tencent/tdesign-vue-next-starter into main

This commit is contained in:
Uyarn 2022-09-05 15:26:01 +08:00
commit 8cba092ee9
14 changed files with 511 additions and 545 deletions

View File

@ -27,7 +27,7 @@
"qrcode.vue": "^3.2.2",
"qs": "^6.10.5",
"tdesign-icons-vue-next": "^0.1.1",
"tdesign-vue-next": "0.20.2",
"tdesign-vue-next": "0.20.3",
"tvision-color": "^1.3.1",
"vue": "^3.2.31",
"vue-clipboard3": "^2.0.0",

View File

@ -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: {

View 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>

View 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>

View 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>

View File

@ -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>;
},
});

View 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 [];
}
// metaorderNo
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>

View File

@ -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>
);
},
});

View 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>

View File

@ -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
View 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>

View File

@ -1,4 +1,4 @@
import Layout from '@/layouts';
import Layout from '@/layouts/index.vue';
import DashboardIcon from '@/assets/assets-slide-dashboard.svg';
export default [

View File

@ -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';

View File

@ -1,4 +1,4 @@
import Layout from '@/layouts';
import Layout from '@/layouts/index.vue';
import LogoutIcon from '@/assets/assets-slide-logout.svg';
export default [
@ -20,6 +20,7 @@ export default [
{
path: '/loginRedirect',
name: 'loginRedirect',
redirect: '/login',
meta: { title: '登录页', icon: LogoutIcon },
component: () => import('@/layouts/blank.vue'),
children: [