mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-24 20:36:37 +08:00
refactor(components): basicLayout布局组件重构完成:根据NavMode拆分为多个布局组件
This commit is contained in:
parent
0e0d559d2f
commit
ffe987832f
@ -5,9 +5,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { setupAppContext } from '@/context';
|
|
||||||
import AppProvider from './AppProvider.vue';
|
import AppProvider from './AppProvider.vue';
|
||||||
|
|
||||||
setupAppContext();
|
|
||||||
</script>
|
</script>
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import type { ScrollbarInst } from 'naive-ui';
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
|
import { useRouteProps } from './route';
|
||||||
|
|
||||||
export function useLayoutConfig() {
|
export function useLayoutConfig() {
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
const { setScrollbarInstance } = useAppStore();
|
||||||
|
const routeProps = useRouteProps();
|
||||||
|
|
||||||
/** 反转sider */
|
/** 反转sider */
|
||||||
const siderInverted = computed(() => theme.navStyle.theme !== 'light');
|
const siderInverted = computed(() => theme.navStyle.theme !== 'light');
|
||||||
@ -30,13 +34,38 @@ export function useLayoutConfig() {
|
|||||||
/** 全局头部和多页签的总高度 */
|
/** 全局头部和多页签的总高度 */
|
||||||
const headerAndMultiTabHeight = computed(() => {
|
const headerAndMultiTabHeight = computed(() => {
|
||||||
const {
|
const {
|
||||||
multiTabStyle: { visible, height: tH },
|
multiTabStyle: { visible, height: tabHeight },
|
||||||
headerStyle: { height: hH }
|
headerStyle: { height: headerHeight }
|
||||||
} = theme;
|
} = theme;
|
||||||
const height = visible ? tH + hH : hH;
|
const height = visible ? headerHeight + tabHeight : headerHeight;
|
||||||
return `${height}px`;
|
return `${height}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 全局侧边栏的样式 */
|
||||||
|
const globalSiderClassAndStyle = {
|
||||||
|
class: 'transition-all duration-300 ease-in-out',
|
||||||
|
style: 'z-index:12;box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 纵向flex布局样式 */
|
||||||
|
const flexColumnStyle = 'display:flex;flex-direction:column;height:100%;';
|
||||||
|
|
||||||
|
/** scrollbar的content的样式 */
|
||||||
|
const scrollbarContentStyle = computed(() => {
|
||||||
|
const { fullPage } = routeProps.value;
|
||||||
|
const height = fullPage ? '100%' : 'auto';
|
||||||
|
return `display:flex;flex-direction:column;height:${height};min-height:100%;`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 滚动条实例 */
|
||||||
|
const scrollbar = ref<ScrollbarInst | null>(null);
|
||||||
|
|
||||||
|
watch(scrollbar, newValue => {
|
||||||
|
if (newValue) {
|
||||||
|
setScrollbarInstance(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
siderInverted,
|
siderInverted,
|
||||||
siderMenuWidth,
|
siderMenuWidth,
|
||||||
@ -44,6 +73,10 @@ export function useLayoutConfig() {
|
|||||||
headerPosition,
|
headerPosition,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
multiTabHeight,
|
multiTabHeight,
|
||||||
headerAndMultiTabHeight
|
headerAndMultiTabHeight,
|
||||||
|
globalSiderClassAndStyle,
|
||||||
|
flexColumnStyle,
|
||||||
|
scrollbarContentStyle,
|
||||||
|
scrollbar
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import type { RouteKey } from '@/interface';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由属性
|
* 路由属性
|
||||||
* @description - 必须要在setup里面调用
|
|
||||||
*/
|
*/
|
||||||
export function useRouteProps() {
|
export function useRouteProps() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -29,7 +28,6 @@ export function useRouteProps() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由查询参数
|
* 路由查询参数
|
||||||
* @description - 必须要在setup里面调用
|
|
||||||
*/
|
*/
|
||||||
export function useRouteQuery() {
|
export function useRouteQuery() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -61,3 +59,17 @@ export function routeNameWatcher(callback: (name: RouteKey) => void) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由全路径变化后的回调
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
export function routeFullPathWatcher(callback: (fullPath: string) => void) {
|
||||||
|
const route = useRoute();
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
newValue => {
|
||||||
|
callback(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -50,6 +50,8 @@ export function useRouterPush(inSetup: boolean = true) {
|
|||||||
if (route) {
|
if (route) {
|
||||||
const { query } = route;
|
const { query } = route;
|
||||||
router.push({ path: routePath('login'), query: { ...query, module } });
|
router.push({ path: routePath('login'), query: { ...query, module } });
|
||||||
|
} else {
|
||||||
|
throw Error('该函数必须在setup里面调用!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-layout class="h-full" :native-scrollbar="false">
|
|
||||||
<n-layout-header :inverted="headerInverted" :position="headerPosition" class="z-11">
|
|
||||||
<global-header :show-logo="true" :show-menu-collape="false" :show-menu="true" class="relative z-2" />
|
|
||||||
<global-tab v-if="theme.multiTabStyle.visible" />
|
|
||||||
</n-layout-header>
|
|
||||||
<header-placeholder />
|
|
||||||
<n-layout-content
|
|
||||||
:native-scrollbar="false"
|
|
||||||
:content-style="{ height: routeProps.fullPage ? '100%' : 'auto' }"
|
|
||||||
class="bg-[#f6f9f8] dark:bg-deep-dark"
|
|
||||||
>
|
|
||||||
<global-content />
|
|
||||||
</n-layout-content>
|
|
||||||
<global-footer />
|
|
||||||
</n-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { NLayout, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { useRouteProps, useLayoutConfig } from '@/composables';
|
|
||||||
import { GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, HeaderPlaceholder } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
|
|
||||||
const routeProps = useRouteProps();
|
|
||||||
const { headerInverted, headerPosition } = useLayoutConfig();
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.global-sider {
|
|
||||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
<style scoped></style>
|
|
@ -1,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-layout :has-sider="true" class="h-full">
|
|
||||||
<n-layout-sider
|
|
||||||
class="global-sider z-12 transition-all duration-200 ease-in-out"
|
|
||||||
:inverted="siderInverted"
|
|
||||||
collapse-mode="width"
|
|
||||||
:collapsed="app.menu.collapsed"
|
|
||||||
:collapsed-width="theme.menuStyle.collapsedWidth"
|
|
||||||
:width="siderMenuWidth"
|
|
||||||
:native-scrollbar="false"
|
|
||||||
@collapse="handleMenuCollapse(true)"
|
|
||||||
@expand="handleMenuCollapse(false)"
|
|
||||||
>
|
|
||||||
<global-logo :show-title="!app.menu.collapsed" class="absolute-lt z-2" />
|
|
||||||
<global-menu :style="{ paddingTop: headerHeight }" />
|
|
||||||
</n-layout-sider>
|
|
||||||
<n-layout-content
|
|
||||||
:native-scrollbar="false"
|
|
||||||
:content-style="{ height: routeProps.fullPage ? '100%' : 'auto' }"
|
|
||||||
class="bg-[#f6f9f8] dark:bg-deep-dark"
|
|
||||||
>
|
|
||||||
<n-layout-header :inverted="headerInverted" :position="headerPosition" class="z-11">
|
|
||||||
<global-header :show-logo="false" :show-menu-collape="true" :show-menu="false" class="relative z-2" />
|
|
||||||
<global-tab v-if="theme.multiTabStyle.visible" />
|
|
||||||
</n-layout-header>
|
|
||||||
<div v-if="theme.fixedHeaderAndTab" :style="{ height: headerAndMultiTabHeight }"></div>
|
|
||||||
<global-content />
|
|
||||||
<global-footer />
|
|
||||||
</n-layout-content>
|
|
||||||
</n-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { NLayout, NLayoutSider, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
|
||||||
import { useRouteProps, useLayoutConfig } from '@/composables';
|
|
||||||
import { GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, GlobalLogo, GlobalMenu } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const app = useAppStore();
|
|
||||||
const { handleMenuCollapse } = useAppStore();
|
|
||||||
const routeProps = useRouteProps();
|
|
||||||
const { siderInverted, siderMenuWidth, headerInverted, headerPosition, headerHeight, headerAndMultiTabHeight } =
|
|
||||||
useLayoutConfig();
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.global-sider {
|
|
||||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="p-16px" :class="{ 'router-view_full-page_height': routeProps.fullPage }">
|
|
||||||
<router-view v-slot="{ Component, route }">
|
|
||||||
<transition :name="theme.pageAnimateType" mode="out-in" appear>
|
|
||||||
<keep-alive :include="cacheRoutes">
|
|
||||||
<component :is="Component" v-if="reload" :key="route.fullPath" :class="{ 'h-full': routeProps.fullPage }" />
|
|
||||||
</keep-alive>
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { cacheRoutes } from '@/router';
|
|
||||||
import { useRouteProps } from '@/composables';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { reload } = useReloadInject();
|
|
||||||
const routeProps = useRouteProps();
|
|
||||||
|
|
||||||
const routeViewFullPageHeight = computed(() => {
|
|
||||||
const {
|
|
||||||
multiTabStyle: { visible, height: tH },
|
|
||||||
headerStyle: { height: hH },
|
|
||||||
footerStyle: { height: fH }
|
|
||||||
} = theme;
|
|
||||||
const height = visible ? tH + hH + fH : hH + fH;
|
|
||||||
return `calc(100% - ${height}px)`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.router-view_full-page_height {
|
|
||||||
height: v-bind(routeViewFullPageHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,8 +0,0 @@
|
|||||||
import DarkMode from './DarkMode/index.vue';
|
|
||||||
import NavMode from './NavMode/index.vue';
|
|
||||||
import SystemTheme from './SystemTheme/index.vue';
|
|
||||||
import PageFunc from './PageFunc/index.vue';
|
|
||||||
import PageView from './PageView/index.vue';
|
|
||||||
import ThemeConfig from './ThemeConfig/index.vue';
|
|
||||||
|
|
||||||
export { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig };
|
|
@ -1,19 +0,0 @@
|
|||||||
import GlobalHeader from './GlobalHeader/index.vue';
|
|
||||||
import GlobalContent from './GlobalContent/index.vue';
|
|
||||||
import GlobalFooter from './GlobalFooter/index.vue';
|
|
||||||
import GlobalLogo from './GlobalLogo/index.vue';
|
|
||||||
import GlobalMenu from './GlobalMenu/index.vue';
|
|
||||||
import GlobalTab from './GlobalTab/index.vue';
|
|
||||||
import HeaderPlaceholder from './HeaderPlaceholder/index.vue';
|
|
||||||
import SettingDrawer from './SettingDrawer/index.vue';
|
|
||||||
|
|
||||||
export {
|
|
||||||
GlobalHeader,
|
|
||||||
GlobalContent,
|
|
||||||
GlobalFooter,
|
|
||||||
GlobalLogo,
|
|
||||||
GlobalMenu,
|
|
||||||
GlobalTab,
|
|
||||||
HeaderPlaceholder,
|
|
||||||
SettingDrawer
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import VerticalLayout from './VerticalLayout/index.vue';
|
|
||||||
import VerticalMixLayout from './VerticalMixLayout/index.vue';
|
|
||||||
import HorizontalLayout from './HorizontalLayout/index.vue';
|
|
||||||
import HorizontalMixLayout from './HorizontalMixLayout/index.vue';
|
|
||||||
|
|
||||||
export { VerticalLayout, VerticalMixLayout, HorizontalLayout, HorizontalMixLayout };
|
|
||||||
export * from './common';
|
|
@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<component :is="layoutComponent[theme.navStyle.mode]" />
|
|
||||||
<setting-drawer />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Component } from 'vue';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import type { NavMode } from '@/interface';
|
|
||||||
import { VerticalLayout, VerticalMixLayout, HorizontalLayout, HorizontalMixLayout, SettingDrawer } from './components';
|
|
||||||
|
|
||||||
type LayoutComponent = {
|
|
||||||
[key in NavMode]: Component;
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
|
|
||||||
const layoutComponent: LayoutComponent = {
|
|
||||||
vertical: VerticalLayout,
|
|
||||||
'vertical-mix': VerticalMixLayout,
|
|
||||||
horizontal: HorizontalLayout,
|
|
||||||
'horizontal-mix': HorizontalMixLayout
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
flex-1 flex-col-stretch
|
|
||||||
p-16px
|
|
||||||
bg-[#f6f9f8]
|
|
||||||
dark:bg-deep-dark
|
|
||||||
transition-backgorund-color
|
|
||||||
duration-300
|
|
||||||
ease-in-out
|
|
||||||
"
|
|
||||||
:class="{ 'overflow-hidden': routeProps.fullPage }"
|
|
||||||
>
|
|
||||||
<router-view v-slot="{ Component, route }">
|
|
||||||
<transition :name="theme.pageAnimateType" mode="out-in" appear>
|
|
||||||
<keep-alive :include="cacheRoutes">
|
|
||||||
<component :is="Component" v-if="reload" :key="route.fullPath" class="flex-1" />
|
|
||||||
</keep-alive>
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { cacheRoutes } from '@/router';
|
|
||||||
import { useRouteProps } from '@/composables';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { reload } = useReloadInject();
|
|
||||||
const routeProps = useRouteProps();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-layout-footer>
|
|
||||||
<div class="flex-center h-48px bg-light dark:bg-dark text-[#333639] dark:text-[#ffffffd1]">
|
|
||||||
Copyright ©2021 Soybean Admin
|
|
||||||
</div>
|
|
||||||
</n-layout-footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NLayoutFooter } from 'naive-ui';
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container class="w-40px h-full" tooltip-content="全屏" @click="toggle">
|
|
||||||
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-16px" />
|
|
||||||
<icon-gridicons-fullscreen v-else class="text-16px" />
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useFullscreen } from '@vueuse/core';
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
|
|
||||||
const { isFullscreen, toggle } = useFullscreen();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container tooltip-content="github" class="w-40px h-full">
|
|
||||||
<a href="https://github.com/honghuangdc/soybean-admin" target="_blank" class="flex-center">
|
|
||||||
<icon-mdi-github class="text-20px text-[#666]" />
|
|
||||||
</a>
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-breadcrumb class="px-12px">
|
|
||||||
<template v-for="breadcrumb in breadcrumbList" :key="breadcrumb.key">
|
|
||||||
<n-breadcrumb-item>
|
|
||||||
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
|
||||||
<span>
|
|
||||||
<Icon
|
|
||||||
v-if="theme.crumbsStyle.showIcon && breadcrumb.iconName"
|
|
||||||
:icon="breadcrumb.iconName"
|
|
||||||
class="inline-block mr-4px text-16px"
|
|
||||||
/>
|
|
||||||
<span>{{ breadcrumb.label }}</span>
|
|
||||||
</span>
|
|
||||||
</n-dropdown>
|
|
||||||
<template v-else>
|
|
||||||
<Icon
|
|
||||||
v-if="theme.crumbsStyle.showIcon && breadcrumb.iconName"
|
|
||||||
:icon="breadcrumb.iconName"
|
|
||||||
class="inline-block mr-4px text-16px"
|
|
||||||
/>
|
|
||||||
<span>{{ breadcrumb.label }}</span>
|
|
||||||
</template>
|
|
||||||
</n-breadcrumb-item>
|
|
||||||
</template>
|
|
||||||
</n-breadcrumb>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import type { RouteLocationMatched } from 'vue-router';
|
|
||||||
import { NBreadcrumb, NBreadcrumbItem, NDropdown } from 'naive-ui';
|
|
||||||
import type { DropdownOption } from 'naive-ui';
|
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { routePath } from '@/router';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import type { RouteKey } from '@/interface';
|
|
||||||
|
|
||||||
type Breadcrumb = DropdownOption & {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
disabled: boolean;
|
|
||||||
routeName: RouteKey;
|
|
||||||
hasChildren: boolean;
|
|
||||||
iconName?: string;
|
|
||||||
children?: Breadcrumb[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const breadcrumbList = computed<Breadcrumb[]>(() => generateBreadcrumb());
|
|
||||||
|
|
||||||
function generateBreadcrumb() {
|
|
||||||
const { matched } = route;
|
|
||||||
return recursionBreadcrumb(matched);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 递归匹配路由获取面包屑数据 */
|
|
||||||
function recursionBreadcrumb(routeMatched: RouteLocationMatched[]) {
|
|
||||||
const list: Breadcrumb[] = [];
|
|
||||||
routeMatched.forEach(item => {
|
|
||||||
if (!item.meta?.isNotMenu) {
|
|
||||||
const routeName = item.name as RouteKey;
|
|
||||||
const breadcrumItem: Breadcrumb = {
|
|
||||||
key: routeName,
|
|
||||||
label: (item.meta?.title as string) || '',
|
|
||||||
disabled: item.path === routePath('root'),
|
|
||||||
routeName,
|
|
||||||
hasChildren: false
|
|
||||||
};
|
|
||||||
if (item.meta?.icon) {
|
|
||||||
breadcrumItem.iconName = item.meta.icon as string;
|
|
||||||
}
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
breadcrumItem.hasChildren = true;
|
|
||||||
breadcrumItem.children = recursionBreadcrumb(item.children as RouteLocationMatched[]);
|
|
||||||
}
|
|
||||||
list.push(breadcrumItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dropdownSelect(optionKey: string) {
|
|
||||||
const key = optionKey as RouteKey;
|
|
||||||
router.push({ name: key });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-menu :value="activeKey" mode="horizontal" :options="menus" @update:value="handleUpdateMenu" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import type { MenuOption } from 'naive-ui';
|
|
||||||
import { NMenu } from 'naive-ui';
|
|
||||||
import { menus } from '@/router';
|
|
||||||
import { GlobalMenuOption } from '@/interface';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const activeKey = computed(() => getActiveKey());
|
|
||||||
|
|
||||||
function getActiveKey() {
|
|
||||||
return route.name as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateMenu(key: string, item: MenuOption) {
|
|
||||||
const menuItem = item as GlobalMenuOption;
|
|
||||||
router.push(menuItem.routePath);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container class="w-40px h-full" @click="toggleMenu">
|
|
||||||
<icon-line-md-menu-unfold-left v-if="app.menu.collapsed" class="text-16px" />
|
|
||||||
<icon-line-md-menu-fold-left v-else class="text-16px" />
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { toggleMenu } = useAppStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container class="w-40px h-full" tooltip-content="项目配置" placement="bottom-end" @click="openSettingDrawer">
|
|
||||||
<icon-mdi-light-cog class="text-16px" />
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
|
|
||||||
const { openSettingDrawer } = useAppStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container class="w-40px" content-class="hover:text-primary" tooltip-content="主题模式" @click="toggleDarkMode">
|
|
||||||
<icon-mdi-moon-waning-crescent v-if="theme.darkMode" class="text-14px" />
|
|
||||||
<icon-mdi-white-balance-sunny v-else class="text-14px" />
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { toggleDarkMode } = useThemeStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,55 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-dropdown :options="options" @select="handleDropdown">
|
|
||||||
<hover-container class="px-12px">
|
|
||||||
<img :src="avatar" class="w-32px h-32px" />
|
|
||||||
<span class="pl-8px text-16px font-medium">Soybean</span>
|
|
||||||
</hover-container>
|
|
||||||
</n-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDropdown, useDialog } from 'naive-ui';
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
import { useRouterPush } from '@/composables';
|
|
||||||
import { iconifyRender, resetAuthStorage } from '@/utils';
|
|
||||||
import avatar from '@/assets/svg/avatar/avatar01.svg';
|
|
||||||
|
|
||||||
type DropdownKey = 'user-center' | 'logout';
|
|
||||||
|
|
||||||
const { toLogin } = useRouterPush();
|
|
||||||
const dialog = useDialog();
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: '用户中心',
|
|
||||||
key: 'user-center',
|
|
||||||
icon: iconifyRender('carbon:user-avatar')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'divider',
|
|
||||||
key: 'divider'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '退出登录',
|
|
||||||
key: 'logout',
|
|
||||||
icon: iconifyRender('carbon:logout')
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function handleDropdown(optionKey: string) {
|
|
||||||
const key = optionKey as DropdownKey;
|
|
||||||
if (key === 'logout') {
|
|
||||||
dialog.info({
|
|
||||||
title: '提示',
|
|
||||||
content: '您确定要退出登录吗?',
|
|
||||||
positiveText: '确定',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: () => {
|
|
||||||
resetAuthStorage();
|
|
||||||
toLogin('pwd-login', 'current');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,9 +0,0 @@
|
|||||||
import GlobalBreadcrumb from './GlobalBreadcrumb.vue';
|
|
||||||
import UserAvatar from './UserAvatar.vue';
|
|
||||||
import MenuCollapse from './MenuCollapse.vue';
|
|
||||||
import ThemeMode from './ThemeMode.vue';
|
|
||||||
import FullScreen from './FullScreen.vue';
|
|
||||||
import SettingDrawerButton from './SettingDrawerButton.vue';
|
|
||||||
import GihubSite from './GihubSite.vue';
|
|
||||||
|
|
||||||
export { GlobalBreadcrumb, UserAvatar, MenuCollapse, ThemeMode, FullScreen, SettingDrawerButton, GihubSite };
|
|
@ -1,87 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header v-if="fixedHeaderAndTab && theme.navStyle.mode !== 'horizontal-mix'" class="header-height w-full"></header>
|
|
||||||
<n-layout-header :inverted="headerInverted" :position="position" :style="{ zIndex }">
|
|
||||||
<div class="global-header header-height flex-y-center w-full">
|
|
||||||
<div v-if="!theme.isVerticalNav" class="menu-width h-full">
|
|
||||||
<global-logo />
|
|
||||||
</div>
|
|
||||||
<div v-if="theme.navStyle.mode !== 'horizontal'" class="flex-1-hidden flex-y-center h-full">
|
|
||||||
<menu-collapse v-if="theme.navStyle.mode !== 'vertical-mix'" />
|
|
||||||
<global-breadcrumb v-if="theme.crumbsStyle.visible" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex-1-hidden flex-y-center h-full"
|
|
||||||
:style="{ justifyContent: theme.menuStyle.horizontalPosition }"
|
|
||||||
>
|
|
||||||
<header-menu />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end h-full">
|
|
||||||
<gihub-site />
|
|
||||||
<full-screen />
|
|
||||||
<theme-mode />
|
|
||||||
<user-avatar />
|
|
||||||
<setting-drawer-button v-if="showSettingButton" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-layout-header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { NLayoutHeader } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import {
|
|
||||||
GlobalBreadcrumb,
|
|
||||||
UserAvatar,
|
|
||||||
MenuCollapse,
|
|
||||||
ThemeMode,
|
|
||||||
FullScreen,
|
|
||||||
GihubSite,
|
|
||||||
SettingDrawerButton
|
|
||||||
} from './components';
|
|
||||||
import { GlobalLogo } from '../common';
|
|
||||||
import HeaderMenu from './components/HeaderMenu.vue';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 层级z-index */
|
|
||||||
zIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
zIndex: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
|
|
||||||
const inverted = computed(() => {
|
|
||||||
return theme.navStyle.theme !== 'light';
|
|
||||||
});
|
|
||||||
const fixedHeaderAndTab = computed(() => theme.fixedHeaderAndTab || theme.navStyle.mode === 'horizontal-mix');
|
|
||||||
const position = computed(() => (fixedHeaderAndTab.value ? 'absolute' : 'static'));
|
|
||||||
const headerInverted = computed(() => {
|
|
||||||
return theme.navStyle.theme !== 'dark' ? inverted.value : !inverted.value;
|
|
||||||
});
|
|
||||||
const headerHeight = computed(() => {
|
|
||||||
const { height } = theme.headerStyle;
|
|
||||||
return `${height}px`;
|
|
||||||
});
|
|
||||||
const menuWidth = computed(() => {
|
|
||||||
const { width } = theme.menuStyle;
|
|
||||||
|
|
||||||
return `${width}px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showSettingButton = import.meta.env.DEV || import.meta.env.VITE_HTTP_ENV === 'STAGING';
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.global-header {
|
|
||||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
|
||||||
}
|
|
||||||
.header-height {
|
|
||||||
height: v-bind(headerHeight);
|
|
||||||
}
|
|
||||||
.menu-width {
|
|
||||||
width: v-bind(menuWidth);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-layout-sider
|
|
||||||
:style="{ zIndex }"
|
|
||||||
:inverted="inverted"
|
|
||||||
collapse-mode="width"
|
|
||||||
:collapsed="app.menu.collapsed"
|
|
||||||
:collapsed-width="theme.menuStyle.collapsedWidth"
|
|
||||||
:width="menuWidth"
|
|
||||||
:native-scrollbar="false"
|
|
||||||
@collapse="handleMenuCollapse(true)"
|
|
||||||
@expand="handleMenuCollapse(false)"
|
|
||||||
>
|
|
||||||
<div class="flex-col-stretch h-full">
|
|
||||||
<global-logo v-if="theme.isVerticalNav" />
|
|
||||||
<n-scrollbar class="flex-1-hidden">
|
|
||||||
<n-menu
|
|
||||||
:value="activeKey"
|
|
||||||
:collapsed="app.menu.collapsed"
|
|
||||||
:collapsed-width="theme.menuStyle.collapsedWidth"
|
|
||||||
:collapsed-icon-size="22"
|
|
||||||
:options="menus"
|
|
||||||
:expanded-keys="expandedKeys"
|
|
||||||
:indent="18"
|
|
||||||
@update:value="handleUpdateMenu"
|
|
||||||
@update:expanded-keys="handleUpdateExpandedKeys"
|
|
||||||
/>
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
|
||||||
</n-layout-sider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { NLayoutSider, NScrollbar, NMenu } from 'naive-ui';
|
|
||||||
import type { MenuOption } from 'naive-ui';
|
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
|
||||||
import { menus } from '@/router';
|
|
||||||
import { GlobalMenuOption } from '@/interface';
|
|
||||||
import { GlobalLogo } from '../../../common';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 层级z-index */
|
|
||||||
zIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
zIndex: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const app = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const { handleMenuCollapse } = useAppStore();
|
|
||||||
|
|
||||||
const inverted = computed(() => {
|
|
||||||
return theme.navStyle.theme !== 'light';
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuWidth = computed(() => {
|
|
||||||
const { collapsed } = app.menu;
|
|
||||||
const { mode } = theme.navStyle;
|
|
||||||
const { collapsedWidth, width, mixWidth } = theme.menuStyle;
|
|
||||||
const modeWidth = mode === 'vertical-mix' ? mixWidth : width;
|
|
||||||
return collapsed ? collapsedWidth : modeWidth;
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeKey = computed(() => route.name as string);
|
|
||||||
const expandedKeys = ref<string[]>(getExpendedKeys());
|
|
||||||
|
|
||||||
function getExpendedKeys() {
|
|
||||||
const keys: string[] = [];
|
|
||||||
route.matched.forEach(item => {
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
keys.push(item.name as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateMenu(key: string, item: MenuOption) {
|
|
||||||
const menuItem = item as GlobalMenuOption;
|
|
||||||
router.push(menuItem.routePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateExpandedKeys(keys: string[]) {
|
|
||||||
expandedKeys.value = keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
expandedKeys.value = getExpendedKeys();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,4 +0,0 @@
|
|||||||
import DefaultSider from './DefaultSider/index.vue';
|
|
||||||
import VerticalMixSider from './VerticalMixSider/index.vue';
|
|
||||||
|
|
||||||
export { DefaultSider, VerticalMixSider };
|
|
@ -1,43 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full" :class="{ 'sider-padding': theme.navStyle.mode === 'horizontal-mix' }">
|
|
||||||
<default-sider v-if="theme.navStyle.mode !== 'vertical-mix'" class="global-sider sider-z-index h-full" />
|
|
||||||
<vertical-mix-sider v-else class="global-sider sider-z-index relative h-full" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { DefaultSider, VerticalMixSider } from './components';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 层级z-index */
|
|
||||||
zIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
zIndex: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
|
|
||||||
const classZIndex = computed(() => props.zIndex);
|
|
||||||
const headerHeight = computed(() => {
|
|
||||||
const { height } = theme.headerStyle;
|
|
||||||
return `${height}px`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.global-sider {
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sider-z-index {
|
|
||||||
z-index: v-bind(classZIndex) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sider-padding {
|
|
||||||
padding-top: v-bind(headerHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="theme.multiTabStyle.mode === 'chrome'" class="flex items-end h-full">
|
|
||||||
<chrome-tab
|
|
||||||
v-for="item in app.multiTab.routes"
|
|
||||||
:key="item.path"
|
|
||||||
:is-active="app.multiTab.activeRoute === item.fullPath"
|
|
||||||
:primary-color="theme.themeColor"
|
|
||||||
:closable="item.name !== ROUTE_HOME.name"
|
|
||||||
:dark-mode="theme.darkMode"
|
|
||||||
@click="handleClickTab(item.fullPath)"
|
|
||||||
@close="removeMultiTab(item.fullPath)"
|
|
||||||
@contextmenu="handleContextMenu($event, item.fullPath)"
|
|
||||||
>
|
|
||||||
{{ item.meta?.title }}
|
|
||||||
</chrome-tab>
|
|
||||||
</div>
|
|
||||||
<div v-if="theme.multiTabStyle.mode === 'button'" class="flex-y-center h-full">
|
|
||||||
<button-tab
|
|
||||||
v-for="item in app.multiTab.routes"
|
|
||||||
:key="item.path"
|
|
||||||
class="mr-10px"
|
|
||||||
:is-active="app.multiTab.activeRoute === item.fullPath"
|
|
||||||
:primary-color="theme.themeColor"
|
|
||||||
:closable="item.name !== ROUTE_HOME.name"
|
|
||||||
:dark-mode="theme.darkMode"
|
|
||||||
@click="handleClickTab(item.fullPath)"
|
|
||||||
@close="removeMultiTab(item.fullPath)"
|
|
||||||
@contextmenu="handleContextMenu($event, item.fullPath)"
|
|
||||||
>
|
|
||||||
{{ item.meta?.title }}
|
|
||||||
</button-tab>
|
|
||||||
</div>
|
|
||||||
<context-menu
|
|
||||||
:visible="dropdownVisible"
|
|
||||||
:current-path="dropdownConfig.currentPath"
|
|
||||||
:x="dropdownConfig.x"
|
|
||||||
:y="dropdownConfig.y"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, nextTick } from 'vue';
|
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
|
||||||
import { ROUTE_HOME } from '@/router';
|
|
||||||
import { ChromeTab, ButtonTab } from '@/components';
|
|
||||||
import { useBoolean } from '@/hooks';
|
|
||||||
import { ContextMenu } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const app = useAppStore();
|
|
||||||
const { removeMultiTab, handleClickTab } = useAppStore();
|
|
||||||
const { bool: dropdownVisible, setTrue: showDropdown, setFalse: hideDropdown } = useBoolean();
|
|
||||||
|
|
||||||
const dropdownConfig = reactive({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
currentPath: ''
|
|
||||||
});
|
|
||||||
function setDropdownConfig(x: number, y: number, currentPath: string) {
|
|
||||||
Object.assign(dropdownConfig, { x, y, currentPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContextMenu(e: MouseEvent, fullPath: string) {
|
|
||||||
e.preventDefault();
|
|
||||||
const { clientX, clientY } = e;
|
|
||||||
hideDropdown();
|
|
||||||
setDropdownConfig(clientX, clientY, fullPath);
|
|
||||||
nextTick(() => {
|
|
||||||
showDropdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hover-container class="w-64px h-full" tooltip-content="重新加载" placement="bottom-end" @click="handleRefresh">
|
|
||||||
<icon-mdi-refresh class="text-16px" :class="{ 'reload-animation': loading }" />
|
|
||||||
</hover-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { HoverContainer } from '@/components';
|
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { useLoading } from '@/hooks';
|
|
||||||
|
|
||||||
const { handleReload } = useReloadInject();
|
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
|
||||||
|
|
||||||
function handleRefresh() {
|
|
||||||
startLoading();
|
|
||||||
handleReload();
|
|
||||||
setTimeout(() => {
|
|
||||||
endLoading();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.reload-animation {
|
|
||||||
animation: rotate 1s;
|
|
||||||
}
|
|
||||||
@keyframes rotate {
|
|
||||||
from {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,153 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-dropdown
|
|
||||||
:show="dropdownVisible"
|
|
||||||
:options="options"
|
|
||||||
placement="bottom-start"
|
|
||||||
:x="x"
|
|
||||||
:y="y"
|
|
||||||
@clickoutside="hide"
|
|
||||||
@select="handleDropdown"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, watch } from 'vue';
|
|
||||||
import { NDropdown } from 'naive-ui';
|
|
||||||
import type { DropdownOption } from 'naive-ui';
|
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { useBoolean } from '@/hooks';
|
|
||||||
import { ROUTE_HOME } from '@/router';
|
|
||||||
import { iconifyRender } from '@/utils';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 右键菜单可见性 */
|
|
||||||
visible?: boolean;
|
|
||||||
/** 当前是否是路由首页 */
|
|
||||||
isRouteHome?: boolean;
|
|
||||||
/** 当前路由路径 */
|
|
||||||
currentPath?: string;
|
|
||||||
/** 鼠标x坐标 */
|
|
||||||
x: number;
|
|
||||||
/** 鼠标y坐标 */
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DropdownKey = 'reload-current' | 'close-current' | 'close-other' | 'close-left' | 'close-right' | 'close-all';
|
|
||||||
type Option = DropdownOption & {
|
|
||||||
key: DropdownKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
visible: false,
|
|
||||||
isRouteHome: false,
|
|
||||||
currentPath: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:visible', visible: boolean): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { removeMultiTab, clearMultiTab, clearLeftMultiTab, clearRightMultiTab } = useAppStore();
|
|
||||||
const { handleReload } = useReloadInject();
|
|
||||||
const { bool: dropdownVisible, setTrue: show, setFalse: hide } = useBoolean(props.visible);
|
|
||||||
|
|
||||||
const options = computed<Option[]>(() => [
|
|
||||||
{
|
|
||||||
label: '重新加载',
|
|
||||||
key: 'reload-current',
|
|
||||||
disabled: props.currentPath !== app.multiTab.activeRoute,
|
|
||||||
icon: iconifyRender('ant-design:reload-outlined')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭标签页',
|
|
||||||
key: 'close-current',
|
|
||||||
disabled: props.currentPath === ROUTE_HOME.path,
|
|
||||||
icon: iconifyRender('ant-design:close-outlined')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭其他标签页',
|
|
||||||
key: 'close-other',
|
|
||||||
icon: iconifyRender('ant-design:column-width-outlined')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭左边标签页',
|
|
||||||
key: 'close-left',
|
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-left')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭右边标签页',
|
|
||||||
key: 'close-right',
|
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-right')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭全部标签页',
|
|
||||||
key: 'close-all',
|
|
||||||
icon: iconifyRender('ant-design:minus-outlined')
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const actionMap = new Map<DropdownKey, () => void>([
|
|
||||||
[
|
|
||||||
'reload-current',
|
|
||||||
() => {
|
|
||||||
handleReload();
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-current',
|
|
||||||
() => {
|
|
||||||
removeMultiTab(props.currentPath);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-other',
|
|
||||||
() => {
|
|
||||||
clearMultiTab([props.currentPath]);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-left',
|
|
||||||
() => {
|
|
||||||
clearLeftMultiTab(props.currentPath);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-right',
|
|
||||||
() => {
|
|
||||||
clearRightMultiTab(props.currentPath);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-all',
|
|
||||||
() => {
|
|
||||||
clearMultiTab();
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
function handleDropdown(optionKey: string) {
|
|
||||||
const key = optionKey as DropdownKey;
|
|
||||||
const actionFunc = actionMap.get(key);
|
|
||||||
if (actionFunc) {
|
|
||||||
actionFunc();
|
|
||||||
}
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
newValue => {
|
|
||||||
if (newValue) {
|
|
||||||
show();
|
|
||||||
} else {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
watch(dropdownVisible, newValue => {
|
|
||||||
emit('update:visible', newValue);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,3 +0,0 @@
|
|||||||
import ContextMenu from './ContextMenu.vue';
|
|
||||||
|
|
||||||
export { ContextMenu };
|
|
@ -1,4 +0,0 @@
|
|||||||
import MultiTab from './MultiTab/index.vue';
|
|
||||||
import ReloadButton from './ReloadButton/index.vue';
|
|
||||||
|
|
||||||
export { MultiTab, ReloadButton };
|
|
@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="fixedHeaderAndTab && theme.navStyle.mode !== 'horizontal-mix'" class="multi-tab-height w-full"></div>
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
multi-tab
|
|
||||||
flex-center
|
|
||||||
justify-between
|
|
||||||
w-full
|
|
||||||
pl-10px
|
|
||||||
bg-light
|
|
||||||
dark:bg-dark
|
|
||||||
transition-backgorund-color
|
|
||||||
duration-300
|
|
||||||
ease-in-out
|
|
||||||
"
|
|
||||||
:class="{ 'multi-tab-top absolute': fixedHeaderAndTab }"
|
|
||||||
:style="{ zIndex }"
|
|
||||||
:align="'center'"
|
|
||||||
justify="space-between"
|
|
||||||
:item-style="{ paddingTop: '0px', paddingBottom: '0px' }"
|
|
||||||
>
|
|
||||||
<div class="flex-1-hidden h-full">
|
|
||||||
<better-scroll :options="{ scrollX: true, scrollY: false, click: isMobile }">
|
|
||||||
<multi-tab />
|
|
||||||
</better-scroll>
|
|
||||||
</div>
|
|
||||||
<reload-button />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, watch } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
|
||||||
import { BetterScroll } from '@/components';
|
|
||||||
import { MultiTab, ReloadButton } from './components';
|
|
||||||
import { useIsMobile } from '@/composables';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 层级z-index */
|
|
||||||
zIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
zIndex: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { initMultiTab, addMultiTab, setActiveMultiTab } = useAppStore();
|
|
||||||
|
|
||||||
const fixedHeaderAndTab = computed(() => theme.fixedHeaderAndTab || theme.navStyle.mode === 'horizontal-mix');
|
|
||||||
const multiTabHeight = computed(() => {
|
|
||||||
const { height } = theme.multiTabStyle;
|
|
||||||
return `${height}px`;
|
|
||||||
});
|
|
||||||
const headerHeight = computed(() => {
|
|
||||||
const { height } = theme.headerStyle;
|
|
||||||
return `${height}px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
initMultiTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
newValue => {
|
|
||||||
addMultiTab(route);
|
|
||||||
setActiveMultiTab(newValue);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.multi-tab {
|
|
||||||
height: v-bind(multiTabHeight);
|
|
||||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
|
||||||
}
|
|
||||||
.multi-tab-height {
|
|
||||||
height: v-bind(multiTabHeight);
|
|
||||||
}
|
|
||||||
.multi-tab-top {
|
|
||||||
top: v-bind(headerHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout :native-scrollbar="false" :content-style="scrollbarContentStyle" class="h-full">
|
||||||
|
<n-layout-header :inverted="headerInverted" :position="headerPosition" :class="{ 'z-11': theme.fixedHeaderAndTab }">
|
||||||
|
<global-header :show-logo="true" :show-menu-collape="false" :show-menu="true" class="relative z-2" />
|
||||||
|
<global-tab v-if="theme.multiTabStyle.visible" />
|
||||||
|
</n-layout-header>
|
||||||
|
<space-placeholder />
|
||||||
|
<n-layout-content ref="scrollbar" class="flex-1" :native-scrollbar="false" :content-style="scrollbarContentStyle">
|
||||||
|
<global-content />
|
||||||
|
<global-footer />
|
||||||
|
</n-layout-content>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NLayout, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useLayoutConfig } from '@/composables';
|
||||||
|
import { GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, SpacePlaceholder } from '../common';
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const { headerInverted, headerPosition, scrollbarContentStyle, scrollbar } = useLayoutConfig();
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.global-sider {
|
||||||
|
box-shadow: var(--global-sider-shadow);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout :native-scrollbar="false" :content-style="flexColumnStyle" class="h-full">
|
||||||
|
<n-layout-header :inverted="headerInverted" position="absolute" class="z-13">
|
||||||
|
<global-header :show-logo="true" :show-menu-collape="true" :show-menu="false" class="relative z-2" />
|
||||||
|
</n-layout-header>
|
||||||
|
<n-layout :has-sider="true" class="h-full">
|
||||||
|
<n-layout-sider
|
||||||
|
v-bind="globalSiderClassAndStyle"
|
||||||
|
:content-style="flexColumnStyle"
|
||||||
|
:inverted="siderInverted"
|
||||||
|
collapse-mode="width"
|
||||||
|
:collapsed="app.menu.collapsed"
|
||||||
|
:collapsed-width="theme.menuStyle.collapsedWidth"
|
||||||
|
:width="siderMenuWidth"
|
||||||
|
:native-scrollbar="false"
|
||||||
|
@collapse="handleMenuCollapse(true)"
|
||||||
|
@expand="handleMenuCollapse(false)"
|
||||||
|
>
|
||||||
|
<space-placeholder :remove-tab="true" />
|
||||||
|
<n-scrollbar class="flex-1-hidden">
|
||||||
|
<global-menu />
|
||||||
|
</n-scrollbar>
|
||||||
|
</n-layout-sider>
|
||||||
|
<n-layout-content ref="scrollbar" :native-scrollbar="false" :content-style="scrollbarContentStyle">
|
||||||
|
<global-tab
|
||||||
|
v-if="theme.multiTabStyle.visible"
|
||||||
|
class="absolute left-0 w-full z-11 bg-white dark:bg-dark transition-background-color duration-300 ease-in-out"
|
||||||
|
:style="{ top: headerHeight }"
|
||||||
|
/>
|
||||||
|
<space-placeholder />
|
||||||
|
<global-content />
|
||||||
|
<global-footer />
|
||||||
|
</n-layout-content>
|
||||||
|
</n-layout>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NLayout, NLayoutContent, NLayoutSider, NLayoutHeader, NScrollbar } from 'naive-ui';
|
||||||
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
|
import { useLayoutConfig } from '@/composables';
|
||||||
|
import { GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, GlobalMenu, SpacePlaceholder } from '../common';
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const app = useAppStore();
|
||||||
|
const { handleMenuCollapse } = useAppStore();
|
||||||
|
const {
|
||||||
|
headerInverted,
|
||||||
|
siderInverted,
|
||||||
|
siderMenuWidth,
|
||||||
|
globalSiderClassAndStyle,
|
||||||
|
flexColumnStyle,
|
||||||
|
scrollbarContentStyle,
|
||||||
|
headerHeight,
|
||||||
|
scrollbar
|
||||||
|
} = useLayoutConfig();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -1,26 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">深色主题</n-divider>
|
|
||||||
<div class="flex-center">
|
|
||||||
<n-switch :value="theme.darkMode" @update:value="handleDarkMode">
|
|
||||||
<template #checked>
|
|
||||||
<icon-mdi-white-balance-sunny class="text-14px text-primary" />
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
<icon-mdi-moon-waning-crescent class="text-14px text-primary" />
|
|
||||||
</template>
|
|
||||||
</n-switch>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDivider, NSwitch } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { handleDarkMode } = useThemeStore();
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
:deep(.n-switch__rail) {
|
|
||||||
background-color: #000e1c !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="nav-type border-2px rounded-6px cursor-pointer"
|
|
||||||
:class="[checked ? 'border-primary' : 'border-transparent']"
|
|
||||||
>
|
|
||||||
<n-tooltip :placement="activeConfig.placement" trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<div class="nav-type-main relative w-56px h-48px bg-[#fff] rounded-4px overflow-hidden">
|
|
||||||
<div class="absolute-lt bg-[#273352]" :class="`${activeConfig.menuClass}`"></div>
|
|
||||||
<div class="absolute-rb bg-[#f0f2f5]" :class="`${activeConfig.mainClass}`"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span>{{ EnumNavMode[mode] }}</span>
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { NTooltip } from 'naive-ui';
|
|
||||||
import type { FollowerPlacement } from 'vueuc';
|
|
||||||
import { EnumNavMode } from '@/enum';
|
|
||||||
import type { NavMode } from '@/interface';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 导航模式 */
|
|
||||||
mode?: NavMode;
|
|
||||||
/** 选中状态 */
|
|
||||||
checked?: boolean;
|
|
||||||
/** 主题颜色 */
|
|
||||||
primaryColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
mode: 'vertical',
|
|
||||||
checked: false,
|
|
||||||
primaryColor: '#409EFF'
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = new Map<NavMode, { placement: FollowerPlacement; menuClass: string; mainClass: string }>([
|
|
||||||
['vertical', { placement: 'bottom-start', menuClass: 'w-1/3 h-full', mainClass: 'w-2/3 h-3/4' }],
|
|
||||||
['vertical-mix', { placement: 'bottom', menuClass: 'w-1/4 h-full', mainClass: 'w-2/3 h-3/4' }],
|
|
||||||
['horizontal', { placement: 'bottom', menuClass: 'w-full h-1/4', mainClass: 'w-full h-3/4' }],
|
|
||||||
['horizontal-mix', { placement: 'bottom-end', menuClass: 'w-full h-1/4', mainClass: 'w-2/3 h-3/4' }]
|
|
||||||
]);
|
|
||||||
|
|
||||||
const activeConfig = computed(() => config.get(props.mode)!);
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.border-primary {
|
|
||||||
border-color: v-bind(primaryColor);
|
|
||||||
}
|
|
||||||
.nav-type:hover {
|
|
||||||
border-color: v-bind(primaryColor);
|
|
||||||
}
|
|
||||||
.nav-type-main {
|
|
||||||
box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.18);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||||||
import NavType from './NavType.vue';
|
|
||||||
|
|
||||||
export { NavType };
|
|
@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">导航栏模式</n-divider>
|
|
||||||
<n-space justify="space-between">
|
|
||||||
<nav-type
|
|
||||||
v-for="item in modeList"
|
|
||||||
:key="item.mode"
|
|
||||||
:mode="item.mode"
|
|
||||||
:checked="theme.navStyle.mode === item.mode"
|
|
||||||
:primary-color="theme.themeColor"
|
|
||||||
@click="setNavMode(item.mode)"
|
|
||||||
/>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDivider, NSpace } from 'naive-ui';
|
|
||||||
import { EnumNavMode } from '@/enum';
|
|
||||||
import type { NavMode } from '@/interface';
|
|
||||||
import { NavType } from './components';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
interface ModeList {
|
|
||||||
mode: NavMode;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { setNavMode } = useThemeStore();
|
|
||||||
|
|
||||||
const modeList: ModeList[] = [
|
|
||||||
{ mode: 'vertical', label: EnumNavMode.vertical },
|
|
||||||
{ mode: 'vertical-mix', label: EnumNavMode['vertical-mix'] },
|
|
||||||
{ mode: 'horizontal', label: EnumNavMode.horizontal },
|
|
||||||
{ mode: 'horizontal-mix', label: EnumNavMode['horizontal-mix'] }
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">界面功能</n-divider>
|
|
||||||
<n-space vertical size="large">
|
|
||||||
<setting-menu-item label="顶部菜单位置">
|
|
||||||
<n-select
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.menuStyle.horizontalPosition"
|
|
||||||
:options="theme.menuStyle.horizontalPositionList"
|
|
||||||
@update:value="handleHorizontalMenuPosition"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="菜单展开宽度">
|
|
||||||
<n-input-number
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.menuStyle.width"
|
|
||||||
:disabled="disabledMenuWidth"
|
|
||||||
:step="10"
|
|
||||||
@update:value="handleMenuWidth"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="左侧混合菜单展开宽度">
|
|
||||||
<n-input-number
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.menuStyle.mixWidth"
|
|
||||||
:disabled="disabledMixMenuWidth"
|
|
||||||
:step="5"
|
|
||||||
@update:value="handleMixMenuWidth"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="固定头部和多页签">
|
|
||||||
<n-switch :value="theme.fixedHeaderAndTab" :disabled="isHorizontalMix" @update:value="handleFixedHeaderAndTab" />
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="头部高度">
|
|
||||||
<n-input-number
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.headerStyle.height"
|
|
||||||
:step="1"
|
|
||||||
@update:value="handleHeaderHeight"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="多页签高度">
|
|
||||||
<n-input-number
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.multiTabStyle.height"
|
|
||||||
:step="1"
|
|
||||||
@update:value="handleMultiTabHeight"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { NDivider, NSpace, NSwitch, NSelect, NInputNumber } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { SettingMenuItem } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const {
|
|
||||||
handleHorizontalMenuPosition,
|
|
||||||
handleFixedHeaderAndTab,
|
|
||||||
handleHeaderHeight,
|
|
||||||
handleMultiTabHeight,
|
|
||||||
handleMenuWidth,
|
|
||||||
handleMixMenuWidth
|
|
||||||
} = useThemeStore();
|
|
||||||
|
|
||||||
const isHorizontalMix = computed(() => theme.navStyle.mode === 'horizontal-mix');
|
|
||||||
const disabledMenuWidth = computed(() => {
|
|
||||||
const { mode } = theme.navStyle;
|
|
||||||
return mode !== 'vertical' && mode !== 'horizontal-mix';
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabledMixMenuWidth = computed(() => theme.navStyle.mode !== 'vertical-mix');
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">界面显示</n-divider>
|
|
||||||
<n-space vertical size="large">
|
|
||||||
<setting-menu-item label="面包屑">
|
|
||||||
<n-switch :value="theme.crumbsStyle.visible" @update:value="handleCrumbsVisible" />
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="面包屑图标">
|
|
||||||
<n-switch :value="theme.crumbsStyle.showIcon" @update:value="handleCrumbsIconVisible" />
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="多页签">
|
|
||||||
<n-switch :value="theme.multiTabStyle.visible" @update:value="handleMultiTabVisible" />
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="多页签风格">
|
|
||||||
<n-select
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.multiTabStyle.mode"
|
|
||||||
:options="theme.multiTabStyle.modeList"
|
|
||||||
@update:value="handleMultiTabMode"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="页面切换动画">
|
|
||||||
<n-switch :value="theme.pageStyle.animate" @update:value="handlePageAnimate" />
|
|
||||||
</setting-menu-item>
|
|
||||||
<setting-menu-item label="页面切换动画类型">
|
|
||||||
<n-select
|
|
||||||
class="w-120px"
|
|
||||||
size="small"
|
|
||||||
:value="theme.pageStyle.animateType"
|
|
||||||
:options="theme.pageStyle.animateTypeList"
|
|
||||||
@update:value="handlePageAnimateType"
|
|
||||||
/>
|
|
||||||
</setting-menu-item>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDivider, NSpace, NSwitch, NSelect } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { SettingMenuItem } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const {
|
|
||||||
handleCrumbsVisible,
|
|
||||||
handleCrumbsIconVisible,
|
|
||||||
handleMultiTabVisible,
|
|
||||||
handleMultiTabMode,
|
|
||||||
handlePageAnimate,
|
|
||||||
handlePageAnimateType
|
|
||||||
} = useThemeStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">系统主题</n-divider>
|
|
||||||
<n-grid :cols="8">
|
|
||||||
<n-grid-item v-for="color in theme.themeColorList" :key="color">
|
|
||||||
<color-block :color="color" :checked="color === theme.themeColor" @click="setThemeColor(color)" />
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDivider, NGrid, NGridItem } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
import { ColorBlock } from '../common';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { setThemeColor } = useThemeStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-divider title-placement="center">主题配置</n-divider>
|
|
||||||
<n-space vertical>
|
|
||||||
<div ref="copyRef" :data-clipboard-text="dataClipboardText">
|
|
||||||
<n-button type="primary" :block="true">拷贝当前配置</n-button>
|
|
||||||
</div>
|
|
||||||
<n-button type="warning" :block="true" @click="handleResetConfig">重置当前配置</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, watch } from 'vue';
|
|
||||||
import { NDivider, NSpace, NButton, useDialog, useMessage } from 'naive-ui';
|
|
||||||
import Clipboard from 'clipboard';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const { setDefaultThemeStore } = useThemeStore();
|
|
||||||
const dialog = useDialog();
|
|
||||||
const message = useMessage();
|
|
||||||
|
|
||||||
const copyRef = ref<HTMLElement | null>(null);
|
|
||||||
const dataClipboardText = ref(getClipboardText());
|
|
||||||
|
|
||||||
function getClipboardText() {
|
|
||||||
return JSON.stringify(theme.$state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResetConfig() {
|
|
||||||
setDefaultThemeStore();
|
|
||||||
message.success('已重置配置,请重新拷贝!');
|
|
||||||
}
|
|
||||||
|
|
||||||
function clipboardEventListener() {
|
|
||||||
const copy = new Clipboard(copyRef.value!);
|
|
||||||
copy.on('success', () => {
|
|
||||||
dialog.success({
|
|
||||||
title: '操作成功',
|
|
||||||
content: '复制成功,请替换 src/settings/theme.json的内容!',
|
|
||||||
positiveText: '确定'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => theme.$state,
|
|
||||||
() => {
|
|
||||||
dataClipboardText.value = getClipboardText();
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
clipboardEventListener();
|
|
||||||
});
|
|
||||||
|
|
||||||
// function handleSuccess() {
|
|
||||||
// window.$dialog?.success({
|
|
||||||
// title: '操作成功',
|
|
||||||
// content: '复制成功,请替换 src/settings/theme.json的内容!',
|
|
||||||
// positiveText: '确定'
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,26 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex-center w-20px h-20px mx-6px mb-8px cursor-pointer rounded-2px" :style="{ backgroundColor: color }">
|
|
||||||
<icon-ic-outline-check
|
|
||||||
v-if="checked"
|
|
||||||
class="text-14px text-white"
|
|
||||||
:class="[isWhite ? 'text-gray-700' : 'text-white']"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
color: string;
|
|
||||||
checked?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
checked: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const whiteColors = ['#ffffff', '#fff', 'rgb(255,255,255)'];
|
|
||||||
const isWhite = computed(() => whiteColors.includes(props.color));
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex-y-center justify-between">
|
|
||||||
<span>{{ label }}</span>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
interface Props {
|
|
||||||
/** 文本 */
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,4 +0,0 @@
|
|||||||
import ColorBlock from './ColorBlock.vue';
|
|
||||||
import SettingMenuItem from './SettingMenuItem.vue';
|
|
||||||
|
|
||||||
export { ColorBlock, SettingMenuItem };
|
|
@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-drawer v-model:show="app.settingDrawer.visible" :width="330">
|
|
||||||
<n-drawer-content title="主题配置" :native-scrollbar="false">
|
|
||||||
<dark-mode />
|
|
||||||
<nav-mode />
|
|
||||||
<system-theme />
|
|
||||||
<page-func />
|
|
||||||
<page-view />
|
|
||||||
<theme-config />
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { NDrawer, NDrawerContent } from 'naive-ui';
|
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig } from './components';
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
</script>
|
|
||||||
<style scoped></style>
|
|
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout :has-sider="true" class="h-full">
|
<n-layout :has-sider="true" class="h-full">
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
class="global-sider z-12 transition-all duration-200 ease-in-out"
|
v-bind="globalSiderClassAndStyle"
|
||||||
|
:content-style="flexColumnStyle"
|
||||||
:inverted="siderInverted"
|
:inverted="siderInverted"
|
||||||
collapse-mode="width"
|
collapse-mode="width"
|
||||||
:collapsed="app.menu.collapsed"
|
:collapsed="app.menu.collapsed"
|
||||||
@ -11,19 +12,21 @@
|
|||||||
@collapse="handleMenuCollapse(true)"
|
@collapse="handleMenuCollapse(true)"
|
||||||
@expand="handleMenuCollapse(false)"
|
@expand="handleMenuCollapse(false)"
|
||||||
>
|
>
|
||||||
<global-logo :show-title="!app.menu.collapsed" class="absolute-lt z-2" />
|
<global-logo :show-title="!app.menu.collapsed" />
|
||||||
<global-menu :style="{ paddingTop: headerHeight }" />
|
<n-scrollbar class="flex-1-hidden">
|
||||||
|
<global-menu />
|
||||||
|
</n-scrollbar>
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
<n-layout-content
|
<n-layout-content ref="scrollbar" :native-scrollbar="false" :content-style="scrollbarContentStyle">
|
||||||
:native-scrollbar="false"
|
<n-layout-header
|
||||||
:content-style="{ height: routeProps.fullPage ? '100%' : 'auto' }"
|
:inverted="headerInverted"
|
||||||
class="bg-[#f6f9f8] dark:bg-deep-dark"
|
:position="headerPosition"
|
||||||
|
:class="{ 'z-11': theme.fixedHeaderAndTab }"
|
||||||
>
|
>
|
||||||
<n-layout-header :inverted="headerInverted" :position="headerPosition" class="z-11">
|
|
||||||
<global-header :show-logo="false" :show-menu-collape="true" :show-menu="false" class="relative z-2" />
|
<global-header :show-logo="false" :show-menu-collape="true" :show-menu="false" class="relative z-2" />
|
||||||
<global-tab v-if="theme.multiTabStyle.visible" />
|
<global-tab v-if="theme.multiTabStyle.visible" />
|
||||||
</n-layout-header>
|
</n-layout-header>
|
||||||
<header-placeholder />
|
<space-placeholder />
|
||||||
<global-content />
|
<global-content />
|
||||||
<global-footer />
|
<global-footer />
|
||||||
</n-layout-content>
|
</n-layout-content>
|
||||||
@ -31,15 +34,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NLayout, NLayoutSider, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
import { NLayout, NLayoutSider, NLayoutContent, NLayoutHeader, NScrollbar } from 'naive-ui';
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
import { useRouteProps, useLayoutConfig } from '@/composables';
|
import { useLayoutConfig } from '@/composables';
|
||||||
import {
|
import {
|
||||||
GlobalHeader,
|
GlobalHeader,
|
||||||
GlobalContent,
|
GlobalContent,
|
||||||
GlobalFooter,
|
GlobalFooter,
|
||||||
GlobalTab,
|
GlobalTab,
|
||||||
HeaderPlaceholder,
|
SpacePlaceholder,
|
||||||
GlobalLogo,
|
GlobalLogo,
|
||||||
GlobalMenu
|
GlobalMenu
|
||||||
} from '../common';
|
} from '../common';
|
||||||
@ -47,11 +50,15 @@ import {
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { handleMenuCollapse } = useAppStore();
|
const { handleMenuCollapse } = useAppStore();
|
||||||
const routeProps = useRouteProps();
|
const {
|
||||||
const { siderInverted, siderMenuWidth, headerInverted, headerPosition, headerHeight } = useLayoutConfig();
|
siderInverted,
|
||||||
|
siderMenuWidth,
|
||||||
|
headerInverted,
|
||||||
|
headerPosition,
|
||||||
|
globalSiderClassAndStyle,
|
||||||
|
flexColumnStyle,
|
||||||
|
scrollbarContentStyle,
|
||||||
|
scrollbar
|
||||||
|
} = useLayoutConfig();
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.global-sider {
|
|
||||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout :has-sider="true" class="h-full">
|
||||||
|
<mix-sider v-bind="globalSiderClassAndStyle" />
|
||||||
|
<n-layout-content ref="scrollbar" :native-scrollbar="false" :content-style="scrollbarContentStyle">
|
||||||
|
<n-layout-header
|
||||||
|
:inverted="headerInverted"
|
||||||
|
:position="headerPosition"
|
||||||
|
:class="{ 'z-11': theme.fixedHeaderAndTab }"
|
||||||
|
>
|
||||||
|
<global-header :show-logo="false" :show-menu-collape="false" :show-menu="false" class="relative z-2" />
|
||||||
|
<global-tab v-if="theme.multiTabStyle.visible" />
|
||||||
|
</n-layout-header>
|
||||||
|
<space-placeholder />
|
||||||
|
<global-content />
|
||||||
|
<global-footer />
|
||||||
|
</n-layout-content>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NLayout, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
import { useLayoutConfig } from '@/composables';
|
||||||
|
import { MixSider, GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, SpacePlaceholder } from '../common';
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const { headerInverted, headerPosition, globalSiderClassAndStyle, scrollbarContentStyle, scrollbar } =
|
||||||
|
useLayoutConfig();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1-hidden bg-[#f6f9f8] dark:bg-deep-dark p-16px transition-all duration-300 ease-in-out">
|
||||||
|
<div class="min-h-full" :class="{ 'h-full overflow-hidden': routeProps.fullPage }">
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<transition :name="theme.pageAnimateType" mode="out-in" appear>
|
||||||
|
<keep-alive :include="cacheRoutes">
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
v-if="app.reloadFlag"
|
||||||
|
:key="route.fullPath"
|
||||||
|
:class="{ 'h-full': routeProps.fullPage }"
|
||||||
|
/>
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cacheRoutes } from '@/router';
|
||||||
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
|
import { useRouteProps } from '@/composables';
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const app = useAppStore();
|
||||||
|
const routeProps = useRouteProps();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout-footer>
|
<n-layout-footer class="bg-white dark:bg-dark">
|
||||||
<div class="footer-height flex-center">Copyright ©2021 Soybean Admin</div>
|
<div class="footer-height flex-center">Copyright ©2021 Soybean Admin</div>
|
||||||
</n-layout-footer>
|
</n-layout-footer>
|
||||||
</template>
|
</template>
|
@ -17,7 +17,6 @@
|
|||||||
<full-screen />
|
<full-screen />
|
||||||
<theme-mode />
|
<theme-mode />
|
||||||
<user-avatar />
|
<user-avatar />
|
||||||
<setting-drawer-button v-if="showSettingButton" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -32,8 +31,7 @@ import {
|
|||||||
MenuCollapse,
|
MenuCollapse,
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
FullScreen,
|
FullScreen,
|
||||||
GithubSite,
|
GithubSite
|
||||||
SettingDrawerButton
|
|
||||||
} from './components';
|
} from './components';
|
||||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||||
|
|
||||||
@ -50,8 +48,6 @@ defineProps<Props>();
|
|||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const { headerHeight } = useLayoutConfig();
|
const { headerHeight } = useLayoutConfig();
|
||||||
|
|
||||||
const showSettingButton = import.meta.env.DEV || import.meta.env.VITE_HTTP_ENV === 'STAGING';
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.global-header {
|
.global-header {
|
@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a href="/" class="logo-height nowrap-hidden flex-center cursor-pointer">
|
|
||||||
<system-logo class="w-32px h-32px" :color="primaryColor" />
|
|
||||||
<h2 v-show="showTitle" class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { SystemLogo } from '@/components';
|
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
|
||||||
import { useAppTitle } from '@/composables';
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const theme = useThemeStore();
|
|
||||||
const title = useAppTitle();
|
|
||||||
|
|
||||||
const showTitle = computed(
|
|
||||||
() => !theme.isVerticalNav || (!app.menu.collapsed && theme.navStyle.mode !== 'vertical-mix')
|
|
||||||
);
|
|
||||||
|
|
||||||
const primaryColor = computed(() => theme.themeColor);
|
|
||||||
const headerHeight = computed(() => {
|
|
||||||
const { height } = theme.headerStyle;
|
|
||||||
return `${height}px`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.logo-height {
|
|
||||||
height: v-bind(headerHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,19 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a
|
<a href="/" class="flex-center w-full nowrap-hidden cursor-pointer" :style="{ height: headerHeight }">
|
||||||
href="/"
|
|
||||||
class="
|
|
||||||
flex-center
|
|
||||||
w-full
|
|
||||||
nowrap-hidden
|
|
||||||
bg-light
|
|
||||||
dark:bg-dark
|
|
||||||
transition-background-color
|
|
||||||
duration-300
|
|
||||||
ease-in-out
|
|
||||||
cursor-pointer
|
|
||||||
"
|
|
||||||
:style="{ height: headerHeight }"
|
|
||||||
>
|
|
||||||
<system-logo class="w-32px h-32px" :color="theme.themeColor" />
|
<system-logo class="w-32px h-32px" :color="theme.themeColor" />
|
||||||
<h2 v-show="showTitle" class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
<h2 v-show="showTitle" class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
||||||
</a>
|
</a>
|
@ -15,7 +15,6 @@ import { computed, watch } from 'vue';
|
|||||||
import { NDropdown } from 'naive-ui';
|
import { NDropdown } from 'naive-ui';
|
||||||
import type { DropdownOption } from 'naive-ui';
|
import type { DropdownOption } from 'naive-ui';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
import { ROUTE_HOME } from '@/router';
|
import { ROUTE_HOME } from '@/router';
|
||||||
import { iconifyRender } from '@/utils';
|
import { iconifyRender } from '@/utils';
|
||||||
@ -49,8 +48,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { removeMultiTab, clearMultiTab, clearLeftMultiTab, clearRightMultiTab } = useAppStore();
|
const { handleReload, removeMultiTab, clearMultiTab, clearLeftMultiTab, clearRightMultiTab } = useAppStore();
|
||||||
const { handleReload } = useReloadInject();
|
|
||||||
const { bool: dropdownVisible, setTrue: show, setFalse: hide } = useBoolean(props.visible);
|
const { bool: dropdownVisible, setTrue: show, setFalse: hide } = useBoolean(props.visible);
|
||||||
|
|
||||||
const options = computed<Option[]>(() => [
|
const options = computed<Option[]>(() => [
|
||||||
@ -61,30 +59,25 @@ const options = computed<Option[]>(() => [
|
|||||||
icon: iconifyRender('ant-design:reload-outlined')
|
icon: iconifyRender('ant-design:reload-outlined')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭标签页',
|
label: '关闭',
|
||||||
key: 'close-current',
|
key: 'close-current',
|
||||||
disabled: props.currentPath === ROUTE_HOME.path,
|
disabled: props.currentPath === ROUTE_HOME.path,
|
||||||
icon: iconifyRender('ant-design:close-outlined')
|
icon: iconifyRender('ant-design:close-outlined')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭其他标签页',
|
label: '关闭其他',
|
||||||
key: 'close-other',
|
key: 'close-other',
|
||||||
icon: iconifyRender('ant-design:column-width-outlined')
|
icon: iconifyRender('ant-design:column-width-outlined')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭左边标签页',
|
label: '关闭左侧',
|
||||||
key: 'close-left',
|
key: 'close-left',
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-left')
|
icon: iconifyRender('mdi:format-horizontal-align-left')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭右边标签页',
|
label: '关闭右侧',
|
||||||
key: 'close-right',
|
key: 'close-right',
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-right')
|
icon: iconifyRender('mdi:format-horizontal-align-right')
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '关闭全部标签页',
|
|
||||||
key: 'close-all',
|
|
||||||
icon: iconifyRender('ant-design:minus-outlined')
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -118,12 +111,6 @@ const actionMap = new Map<DropdownKey, () => void>([
|
|||||||
() => {
|
() => {
|
||||||
clearRightMultiTab(props.currentPath);
|
clearRightMultiTab(props.currentPath);
|
||||||
}
|
}
|
||||||
],
|
|
||||||
[
|
|
||||||
'close-all',
|
|
||||||
() => {
|
|
||||||
clearMultiTab();
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
@ -44,7 +44,7 @@ import { useThemeStore, useAppStore } from '@/store';
|
|||||||
import { ROUTE_HOME } from '@/router';
|
import { ROUTE_HOME } from '@/router';
|
||||||
import { ChromeTab, ButtonTab } from '@/components';
|
import { ChromeTab, ButtonTab } from '@/components';
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
import { ContextMenu } from '../common';
|
import { ContextMenu } from './components';
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { HoverContainer } from '@/components';
|
import { HoverContainer } from '@/components';
|
||||||
import { useReloadInject } from '@/context';
|
import { useAppStore } from '@/store';
|
||||||
import { useLoading } from '@/hooks';
|
import { useLoading } from '@/hooks';
|
||||||
|
|
||||||
const { handleReload } = useReloadInject();
|
const { handleReload } = useAppStore();
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="multi-tab flex-center w-full pl-16px" :style="{ height: multiTabHeight }">
|
<div class="multi-tab flex-center w-full pl-16px" :style="{ height: multiTabHeight }">
|
||||||
<div class="flex-1-hidden h-full">
|
<div class="flex-1-hidden h-full">
|
||||||
<better-scroll :options="{ scrollX: true, scrollY: false, click: true }">
|
<better-scroll :options="{ scrollX: true, scrollY: false, click: isMobile }">
|
||||||
<multi-tab />
|
<multi-tab />
|
||||||
</better-scroll>
|
</better-scroll>
|
||||||
</div>
|
</div>
|
||||||
@ -10,28 +10,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { useLayoutConfig } from '@/composables';
|
import { useLayoutConfig, routeFullPathWatcher, useIsMobile } from '@/composables';
|
||||||
import { BetterScroll } from '@/components';
|
import { BetterScroll } from '@/components';
|
||||||
import { MultiTab, ReloadButton } from './components';
|
import { MultiTab, ReloadButton } from './components';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { initMultiTab, addMultiTab, setActiveMultiTab } = useAppStore();
|
const { initMultiTab, addMultiTab, setActiveMultiTab } = useAppStore();
|
||||||
const { multiTabHeight } = useLayoutConfig();
|
const { multiTabHeight } = useLayoutConfig();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
initMultiTab();
|
initMultiTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
routeFullPathWatcher(fullPath => {
|
||||||
() => route.fullPath,
|
|
||||||
newValue => {
|
|
||||||
addMultiTab(route);
|
addMultiTab(route);
|
||||||
setActiveMultiTab(newValue);
|
setActiveMultiTab(fullPath);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
init();
|
init();
|
@ -6,6 +6,15 @@
|
|||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useLayoutConfig } from '@/composables';
|
import { useLayoutConfig } from '@/composables';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 去除tab的高度 */
|
||||||
|
removeTab?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
removeTab: false
|
||||||
|
});
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const { headerAndMultiTabHeight } = useLayoutConfig();
|
const { headerAndMultiTabHeight } = useLayoutConfig();
|
||||||
</script>
|
</script>
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-center h-36px text-[#333639] dark:text-[#ffffffd1] cursor-pointer" @click="toggleMenu">
|
<div class="flex-center h-36px text-[#333639] dark:text-[#ffffffd1] cursor-pointer" @click="toggleMenu">
|
||||||
<icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
|
<icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
|
||||||
<icon-ph:caret-double-left-bold v-else class="text-16px" />
|
<icon-ph-caret-double-left-bold v-else class="text-16px" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -6,19 +6,19 @@
|
|||||||
flex-col-stretch
|
flex-col-stretch
|
||||||
h-full
|
h-full
|
||||||
overflow-hidden
|
overflow-hidden
|
||||||
|
bg-white
|
||||||
|
dark:bg-dark
|
||||||
transition-width
|
transition-width
|
||||||
duration-300
|
duration-300
|
||||||
ease-in-out
|
ease-in-out
|
||||||
bg-white
|
|
||||||
dark:bg-dark
|
|
||||||
"
|
"
|
||||||
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
>
|
>
|
||||||
<header class="header-height flex-y-center justify-between">
|
<header class="header-height flex-y-center justify-between">
|
||||||
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
||||||
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="toggleFixedMixMenu">
|
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="toggleFixedMixMenu">
|
||||||
<icon-mdi:pin-off v-if="app.menu.fixedMix" />
|
<icon-mdi-pin-off v-if="app.menu.fixedMix" />
|
||||||
<icon-mdi:pin v-else />
|
<icon-mdi-pin v-else />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-1-hidden">
|
<div class="flex-1-hidden">
|
@ -2,9 +2,9 @@
|
|||||||
<div class="flex h-full bg-white dark:bg-[#18181c]" @mouseleave="handleMouseLeaveMenu">
|
<div class="flex h-full bg-white dark:bg-[#18181c]" @mouseleave="handleMouseLeaveMenu">
|
||||||
<div
|
<div
|
||||||
class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
|
class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
|
||||||
:class="[app.menu.collapsed ? 'mix-menu-collapsed-width' : 'mix-menu-width']"
|
:class="[app.menu.collapsed ? 'mix-menu_collapsed-width' : 'mix-menu_width']"
|
||||||
>
|
>
|
||||||
<global-logo />
|
<global-logo :show-title="false" />
|
||||||
<div class="flex-1-hidden">
|
<div class="flex-1-hidden">
|
||||||
<n-scrollbar>
|
<n-scrollbar>
|
||||||
<mix-menu
|
<mix-menu
|
||||||
@ -31,15 +31,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import type { VNodeChild } from 'vue';
|
import type { VNodeChild } from 'vue';
|
||||||
import { NScrollbar } from 'naive-ui';
|
import { NScrollbar } from 'naive-ui';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
import { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
import { routeNameWatcher } from '@/composables';
|
||||||
import { GlobalLogo } from '../../../common';
|
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
||||||
|
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
@ -89,18 +90,15 @@ function handleMouseLeaveMenu() {
|
|||||||
hideDrawer();
|
hideDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
routeNameWatcher(() => {
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
activeParentRouteName.value = getActiveRouteName();
|
activeParentRouteName.value = getActiveRouteName();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mix-menu-width {
|
.mix-menu_width {
|
||||||
width: v-bind(mixMenuWidth);
|
width: v-bind(mixMenuWidth);
|
||||||
}
|
}
|
||||||
.mix-menu-collapsed-width {
|
.mix-menu_collapsed-width {
|
||||||
width: v-bind(mixMenuCollapsedWidth);
|
width: v-bind(mixMenuCollapsedWidth);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
fixed
|
||||||
|
flex-center
|
||||||
|
top-240px
|
||||||
|
right-14px
|
||||||
|
z-10000
|
||||||
|
w-48px
|
||||||
|
h-48px
|
||||||
|
bg-primary
|
||||||
|
rounded-4px
|
||||||
|
cursor-pointer
|
||||||
|
transition-right
|
||||||
|
duration-300
|
||||||
|
ease-in-out
|
||||||
|
"
|
||||||
|
:class="{ '!right-330px': app.settingDrawer.visible }"
|
||||||
|
@click="handleClickButton"
|
||||||
|
>
|
||||||
|
<icon-ic:round-close v-if="app.settingDrawer.visible" class="z-20 text-24px text-white" />
|
||||||
|
<icon-ic-round-settings v-else class="z-20 text-24px text-white" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
|
const { openSettingDrawer, closeSettingDrawer } = useAppStore();
|
||||||
|
|
||||||
|
function handleClickButton() {
|
||||||
|
if (app.settingDrawer.visible) {
|
||||||
|
closeSettingDrawer();
|
||||||
|
} else {
|
||||||
|
openSettingDrawer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -4,5 +4,6 @@ import SystemTheme from './SystemTheme/index.vue';
|
|||||||
import PageFunc from './PageFunc/index.vue';
|
import PageFunc from './PageFunc/index.vue';
|
||||||
import PageView from './PageView/index.vue';
|
import PageView from './PageView/index.vue';
|
||||||
import ThemeConfig from './ThemeConfig/index.vue';
|
import ThemeConfig from './ThemeConfig/index.vue';
|
||||||
|
import DrawerButton from './DrawerButton/index.vue';
|
||||||
|
|
||||||
export { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig };
|
export { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig, DrawerButton };
|
@ -9,13 +9,16 @@
|
|||||||
<theme-config />
|
<theme-config />
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
|
<drawer-button v-if="showSettingButton" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NDrawer, NDrawerContent } from 'naive-ui';
|
import { NDrawer, NDrawerContent } from 'naive-ui';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig } from './components';
|
import { DarkMode, NavMode, SystemTheme, PageFunc, PageView, ThemeConfig, DrawerButton } from './components';
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
|
const showSettingButton = import.meta.env.DEV || import.meta.env.VITE_HTTP_ENV === 'STAGING';
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="theme.fixedHeaderAndTab" class="space-placholder_height"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 去除tab的高度 */
|
||||||
|
removeHeader?: boolean;
|
||||||
|
/** 去除tab的高度 */
|
||||||
|
removeTab?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
removeHeader: false,
|
||||||
|
removeTab: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const spaceHeight = computed(() => {
|
||||||
|
const {
|
||||||
|
multiTabStyle: { visible, height: tabHeight },
|
||||||
|
headerStyle: { height: headerHeight }
|
||||||
|
} = theme;
|
||||||
|
let height = 0;
|
||||||
|
if (!props.removeHeader) {
|
||||||
|
height += headerHeight;
|
||||||
|
}
|
||||||
|
if (!props.removeTab && visible) {
|
||||||
|
height += tabHeight;
|
||||||
|
}
|
||||||
|
return `${height}px`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.space-placholder_height {
|
||||||
|
height: v-bind(spaceHeight);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-6px px-4px cursor-pointer" @mouseenter="setTrue" @mouseleave="setFalse">
|
||||||
|
<div
|
||||||
|
class="flex-center flex-col py-12px rounded-2px"
|
||||||
|
:class="{ 'text-primary bg-primary-active': isActive, 'text-primary': isHover }"
|
||||||
|
>
|
||||||
|
<component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
|
||||||
|
<p
|
||||||
|
class="pt-8px text-12px overflow-hidden transition-height duration-200 ease-in-out"
|
||||||
|
:class="[isMini ? 'h-0 pt-0' : 'h-20px pt-8px']"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { VNodeChild } from 'vue';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 路由名称 */
|
||||||
|
routeName: string;
|
||||||
|
/** 路由名称文本 */
|
||||||
|
label: string;
|
||||||
|
/** 路由图标 */
|
||||||
|
icon: VNodeChild;
|
||||||
|
/** 当前激活状态的理由名称 */
|
||||||
|
activeRouteName: string;
|
||||||
|
/** mini尺寸的路由 */
|
||||||
|
isMini?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isMini: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||||
|
|
||||||
|
const isActive = computed(() => props.routeName === props.activeRouteName);
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-center h-36px text-[#333639] dark:text-[#ffffffd1] cursor-pointer" @click="toggleMenu">
|
||||||
|
<icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
|
||||||
|
<icon-ph-caret-double-left-bold v-else class="text-16px" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
|
const { toggleMenu } = useAppStore();
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
drawer-shadow
|
||||||
|
absolute-lt
|
||||||
|
flex-col-stretch
|
||||||
|
h-full
|
||||||
|
overflow-hidden
|
||||||
|
bg-white
|
||||||
|
dark:bg-dark
|
||||||
|
transition-width
|
||||||
|
duration-300
|
||||||
|
ease-in-out
|
||||||
|
"
|
||||||
|
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
|
>
|
||||||
|
<header class="header-height flex-y-center justify-between">
|
||||||
|
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
||||||
|
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="toggleFixedMixMenu">
|
||||||
|
<icon-mdi-pin-off v-if="app.menu.fixedMix" />
|
||||||
|
<icon-mdi-pin v-else />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex-1-hidden">
|
||||||
|
<n-scrollbar>
|
||||||
|
<n-menu :value="activeKey" :options="childMenus" :indent="18" @update:value="handleUpdateMenu" />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { NScrollbar, NMenu } from 'naive-ui';
|
||||||
|
import type { MenuOption } from 'naive-ui';
|
||||||
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
|
import { useAppTitle } from '@/composables';
|
||||||
|
import { menus } from '@/router';
|
||||||
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 菜单抽屉可见性 */
|
||||||
|
visible?: boolean;
|
||||||
|
/** 激活状态的路由名称 */
|
||||||
|
activeRouteName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const app = useAppStore();
|
||||||
|
const { toggleFixedMixMenu } = useAppStore();
|
||||||
|
const title = useAppTitle();
|
||||||
|
|
||||||
|
const childMenus = computed(() => {
|
||||||
|
const children: MenuOption[] = [];
|
||||||
|
menus.some(item => {
|
||||||
|
const flag = item.routeName === props.activeRouteName && Boolean(item.children?.length);
|
||||||
|
if (flag) {
|
||||||
|
children.push(...item.children!);
|
||||||
|
}
|
||||||
|
return flag;
|
||||||
|
});
|
||||||
|
return children;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDrawer = computed(() => (props.visible && childMenus.value.length) || app.menu.fixedMix);
|
||||||
|
|
||||||
|
const activeKey = computed(() => route.name as string);
|
||||||
|
|
||||||
|
function handleUpdateMenu(key: string, item: MenuOption) {
|
||||||
|
const menuItem = item as GlobalMenuOption;
|
||||||
|
router.push(menuItem.routePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerHeight = computed(() => {
|
||||||
|
const { height } = theme.headerStyle;
|
||||||
|
return `${height}px`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.drawer-shadow {
|
||||||
|
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||||
|
}
|
||||||
|
.header-height {
|
||||||
|
height: v-bind(headerHeight);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,5 @@
|
|||||||
|
import MixMenu from './MixMenu.vue';
|
||||||
|
import MixMenuCollapse from './MixMenuCollapse.vue';
|
||||||
|
import MixMenuDrawer from './MixMenuDrawer.vue';
|
||||||
|
|
||||||
|
export { MixMenu, MixMenuCollapse, MixMenuDrawer };
|
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full bg-white dark:bg-[#18181c]" @mouseleave="handleMouseLeaveMenu">
|
||||||
|
<div
|
||||||
|
class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
|
||||||
|
:class="[app.menu.collapsed ? 'mix-menu_collapsed-width' : 'mix-menu_width']"
|
||||||
|
>
|
||||||
|
<global-logo :show-title="false" />
|
||||||
|
<div class="flex-1-hidden">
|
||||||
|
<n-scrollbar>
|
||||||
|
<mix-menu
|
||||||
|
v-for="item in firstDegreeMenus"
|
||||||
|
:key="item.routeName"
|
||||||
|
:route-name="item.routeName"
|
||||||
|
:label="item.label"
|
||||||
|
:icon="item.icon"
|
||||||
|
:active-route-name="activeParentRouteName"
|
||||||
|
:is-mini="app.menu.collapsed"
|
||||||
|
@click="handleMixMenu(item.routeName, item.isSingle)"
|
||||||
|
/>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
<mix-menu-collapse />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative h-full transition-width duration-300 ease-in-out"
|
||||||
|
:style="{ width: app.menu.fixedMix ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
|
>
|
||||||
|
<mix-menu-drawer :visible="drawerVisible" :active-route-name="activeParentRouteName" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { VNodeChild } from 'vue';
|
||||||
|
import { NScrollbar } from 'naive-ui';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
|
import { menus } from '@/router';
|
||||||
|
import { routeNameWatcher } from '@/composables';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
||||||
|
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||||
|
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const app = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { bool: drawerVisible, setTrue: openDrawer, setFalse: hideDrawer } = useBoolean();
|
||||||
|
|
||||||
|
const mixMenuWidth = computed(() => `${theme.menuStyle.mixWidth}px`);
|
||||||
|
const mixMenuCollapsedWidth = computed(() => `${theme.menuStyle.mixCollapsedWidth}px`);
|
||||||
|
|
||||||
|
const firstDegreeMenus = menus.map(item => {
|
||||||
|
const { routeName } = item;
|
||||||
|
const label = item.label as string;
|
||||||
|
const icon = item.icon! as () => VNodeChild;
|
||||||
|
const isSingle = !(item.children && item.children.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
routeName,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
isSingle
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeParentRouteName = ref(getActiveRouteName());
|
||||||
|
|
||||||
|
function getActiveRouteName() {
|
||||||
|
let name = '';
|
||||||
|
const menuMatched = route.matched.filter(item => !item.meta.isNotMenu);
|
||||||
|
if (menuMatched.length) {
|
||||||
|
name = menuMatched[0].name as string;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMixMenu(routeName: string, isSingle: boolean) {
|
||||||
|
activeParentRouteName.value = routeName;
|
||||||
|
if (isSingle) {
|
||||||
|
router.push({ name: routeName });
|
||||||
|
} else {
|
||||||
|
openDrawer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeaveMenu() {
|
||||||
|
activeParentRouteName.value = getActiveRouteName();
|
||||||
|
hideDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
routeNameWatcher(() => {
|
||||||
|
activeParentRouteName.value = getActiveRouteName();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.mix-menu_width {
|
||||||
|
width: v-bind(mixMenuWidth);
|
||||||
|
}
|
||||||
|
.mix-menu_collapsed-width {
|
||||||
|
width: v-bind(mixMenuCollapsedWidth);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,3 +1,25 @@
|
|||||||
import GlobalLogo from './GlobalLogo.vue';
|
import GlobalHeader from './GlobalHeader/index.vue';
|
||||||
|
import GlobalContent from './GlobalContent/index.vue';
|
||||||
|
import GlobalFooter from './GlobalFooter/index.vue';
|
||||||
|
import GlobalLogo from './GlobalLogo/index.vue';
|
||||||
|
import GlobalMenu from './GlobalMenu/index.vue';
|
||||||
|
import GlobalTab from './GlobalTab/index.vue';
|
||||||
|
import VerticalMixSider from './VerticalMixSider/index.vue';
|
||||||
|
import MixSider from './MixSider/index.vue';
|
||||||
|
import SpacePlaceholder from './SpacePlaceholder/index.vue';
|
||||||
|
import HeaderPlaceholder from './HeaderPlaceholder/index.vue';
|
||||||
|
import SettingDrawer from './SettingDrawer/index.vue';
|
||||||
|
|
||||||
export { GlobalLogo };
|
export {
|
||||||
|
GlobalHeader,
|
||||||
|
GlobalContent,
|
||||||
|
GlobalFooter,
|
||||||
|
GlobalLogo,
|
||||||
|
GlobalMenu,
|
||||||
|
GlobalTab,
|
||||||
|
VerticalMixSider,
|
||||||
|
MixSider,
|
||||||
|
SpacePlaceholder,
|
||||||
|
HeaderPlaceholder,
|
||||||
|
SettingDrawer
|
||||||
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import GlobalSider from './GlobalSider/index.vue';
|
import VerticalLayout from './VerticalLayout/index.vue';
|
||||||
import GlobalHeader from './GlobalHeader/index.vue';
|
import VerticalMixLayout from './VerticalMixLayout/index.vue';
|
||||||
import GlobalTab from './GlobalTab/index.vue';
|
import HorizontalLayout from './HorizontalLayout/index.vue';
|
||||||
import GlobalContent from './GlobalContent/index.vue';
|
import HorizontalMixLayout from './HorizontalMixLayout/index.vue';
|
||||||
import GlobalFooter from './GlobalFooter/index.vue';
|
|
||||||
import SettingDrawer from './SettingDrawer/index.vue';
|
|
||||||
|
|
||||||
export { GlobalSider, GlobalHeader, GlobalTab, GlobalContent, GlobalFooter, SettingDrawer };
|
export { VerticalLayout, VerticalMixLayout, HorizontalLayout, HorizontalMixLayout };
|
||||||
|
export * from './common';
|
||||||
|
@ -1,51 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout class="h-full" has-sider>
|
<component :is="layoutComponent[theme.navStyle.mode]" />
|
||||||
<global-sider v-if="theme.isVerticalNav" :z-index="13" />
|
|
||||||
<global-header v-if="isHorizontalMix" :z-index="14" />
|
|
||||||
<div class="flex-1-hidden flex h-full">
|
|
||||||
<global-sider v-if="isHorizontalMix" :z-index="13" />
|
|
||||||
<n-scrollbar class="h-full" :content-class="routeProps.fullPage ? 'h-full' : ''">
|
|
||||||
<div
|
|
||||||
class="inline-flex-col-stretch w-full"
|
|
||||||
:class="[{ 'content-padding': isHorizontalMix }, routeProps.fullPage ? 'h-full' : 'min-h-100vh']"
|
|
||||||
>
|
|
||||||
<global-header v-if="!isHorizontalMix" :z-index="12" />
|
|
||||||
<global-tab v-if="theme.multiTabStyle.visible" :z-index="11" />
|
|
||||||
<global-content />
|
|
||||||
<global-footer />
|
|
||||||
</div>
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
|
||||||
<setting-drawer />
|
<setting-drawer />
|
||||||
</n-layout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import { NLayout, NScrollbar } from 'naive-ui';
|
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { useRouteProps } from '@/composables';
|
import type { NavMode } from '@/interface';
|
||||||
import { GlobalSider, GlobalHeader, GlobalTab, GlobalContent, GlobalFooter, SettingDrawer } from './components';
|
import { VerticalLayout, VerticalMixLayout, HorizontalLayout, HorizontalMixLayout, SettingDrawer } from './components';
|
||||||
|
|
||||||
|
type LayoutComponent = {
|
||||||
|
[key in NavMode]: Component;
|
||||||
|
};
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const routeProps = useRouteProps();
|
|
||||||
|
|
||||||
const isHorizontalMix = computed(() => theme.navStyle.mode === 'horizontal-mix');
|
const layoutComponent: LayoutComponent = {
|
||||||
|
vertical: VerticalLayout,
|
||||||
const headerAndMultiTabHeight = computed(() => {
|
'vertical-mix': VerticalMixLayout,
|
||||||
const {
|
horizontal: HorizontalLayout,
|
||||||
headerStyle: { height: hHeight },
|
'horizontal-mix': HorizontalMixLayout
|
||||||
multiTabStyle: { height: mHeight }
|
};
|
||||||
} = theme;
|
|
||||||
return `${hHeight + mHeight}px`;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style></style>
|
||||||
:deep(.n-scrollbar-rail) {
|
|
||||||
z-index: 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-padding {
|
|
||||||
padding-top: v-bind(headerAndMultiTabHeight);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-scrollbar ref="scrollbar" class="h-full" :content-class="routeProps.fullPage ? 'h-full' : ''">
|
<n-scrollbar ref="scrollbar" class="h-full" :content-class="routeProps.fullPage ? 'h-full' : ''">
|
||||||
<div class="inline-block wh-full bg-[#F6F9F8]">
|
<div class="inline-block wh-full bg-[#f6f9f8]">
|
||||||
<router-view v-slot="{ Component, route: itemRoute }">
|
<router-view v-slot="{ Component, route: itemRoute }">
|
||||||
<transition :name="theme.pageAnimateType" mode="out-in" appear>
|
<transition :name="theme.pageAnimateType" mode="out-in" appear>
|
||||||
<keep-alive :include="cacheRoutes">
|
<keep-alive :include="cacheRoutes">
|
||||||
<component
|
<component
|
||||||
:is="Component"
|
:is="Component"
|
||||||
v-if="reload"
|
v-if="app.reloadFlag"
|
||||||
:key="itemRoute.fullPath"
|
:key="itemRoute.fullPath"
|
||||||
:class="{ 'min-h-100vh': !routeProps.fullPage }"
|
:class="{ 'min-h-100vh': !routeProps.fullPage }"
|
||||||
/>
|
/>
|
||||||
@ -19,13 +19,12 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NScrollbar } from 'naive-ui';
|
import { NScrollbar } from 'naive-ui';
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
import { useReloadInject } from '@/context';
|
|
||||||
import { cacheRoutes } from '@/router';
|
import { cacheRoutes } from '@/router';
|
||||||
import { useRouteProps } from '@/composables';
|
import { useRouteProps } from '@/composables';
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const { reload } = useReloadInject();
|
const app = useAppStore();
|
||||||
|
|
||||||
const routeProps = useRouteProps();
|
const routeProps = useRouteProps();
|
||||||
</script>
|
</script>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user