mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-18 17:46:38 +08:00
refactor(projects): basicLayout重构初步
This commit is contained in:
parent
7925a69b64
commit
33770d2356
@ -19,6 +19,8 @@ export interface ThemeSettings {
|
||||
multiTabStyle: MultiTabStyle;
|
||||
/** 面包屑样式 */
|
||||
crumbsStyle: CrumbsStyle;
|
||||
/** 底部样式 */
|
||||
footerStyle: FooterStyle;
|
||||
/** 页面样式 */
|
||||
pageStyle: PageStyle;
|
||||
/** 固定头部和多页签 */
|
||||
@ -105,6 +107,11 @@ interface CrumbsStyle {
|
||||
showIcon: boolean;
|
||||
}
|
||||
|
||||
interface FooterStyle {
|
||||
/** 底部高度 */
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type AnimateType = keyof typeof EnumAnimate;
|
||||
|
||||
interface AnimateTypeList {
|
||||
|
11
src/layouts/BaseLayout/components/HorizontalLayout/index.vue
Normal file
11
src/layouts/BaseLayout/components/HorizontalLayout/index.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<h3>horizontal</h3>
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped></style>
|
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<h3>horizontal-mix</h3>
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped></style>
|
101
src/layouts/BaseLayout/components/VerticalLayout/index.vue
Normal file
101
src/layouts/BaseLayout/components/VerticalLayout/index.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<n-layout :has-sider="true" class="h-full">
|
||||
<n-layout-sider
|
||||
class="global-sider z-12"
|
||||
: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)"
|
||||
>
|
||||
<global-logo :show-title="!app.menu.collapsed" class="header-height absolute-lt w-full z-2" />
|
||||
<global-menu class="header-padding" />
|
||||
</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="header-height relative z-2"
|
||||
/>
|
||||
<global-tab v-if="theme.multiTabStyle.visible" class="tab-height" />
|
||||
</n-layout-header>
|
||||
<div v-if="theme.fixedHeaderAndTab" class="header-tab_height"></div>
|
||||
<global-content />
|
||||
<global-footer />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { NLayout, NLayoutSider, NLayoutContent, NLayoutHeader } from 'naive-ui';
|
||||
import { useThemeStore, useAppStore } from '@/store';
|
||||
import { useRouteProps } from '@/hooks';
|
||||
import { GlobalHeader, GlobalContent, GlobalFooter, GlobalTab, GlobalLogo, GlobalMenu } from '../common';
|
||||
|
||||
const theme = useThemeStore();
|
||||
const app = useAppStore();
|
||||
const routeProps = useRouteProps();
|
||||
|
||||
const { handleMenuCollapse } = useAppStore();
|
||||
|
||||
const inverted = computed(() => {
|
||||
return theme.navStyle.theme !== 'light';
|
||||
});
|
||||
|
||||
const menuWidth = computed(() => {
|
||||
const { collapsed } = app.menu;
|
||||
const { collapsedWidth, width } = theme.menuStyle;
|
||||
return collapsed ? collapsedWidth : width;
|
||||
});
|
||||
|
||||
const headerInverted = computed(() => {
|
||||
return theme.navStyle.theme !== 'dark' ? inverted.value : !inverted.value;
|
||||
});
|
||||
const headerPosition = computed(() => (theme.fixedHeaderAndTab ? 'absolute' : 'static'));
|
||||
const headerHeight = computed(() => {
|
||||
const { height } = theme.headerStyle;
|
||||
return `${height}px`;
|
||||
});
|
||||
|
||||
const tabHeight = computed(() => {
|
||||
const { height } = theme.multiTabStyle;
|
||||
return `${height}px`;
|
||||
});
|
||||
|
||||
const headerAndTabHeight = computed(() => {
|
||||
const {
|
||||
multiTabStyle: { visible, height: tH },
|
||||
headerStyle: { height: hH }
|
||||
} = theme;
|
||||
const height = visible ? tH + hH : hH;
|
||||
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%);
|
||||
}
|
||||
.header-height {
|
||||
height: v-bind(headerHeight);
|
||||
}
|
||||
.header-padding {
|
||||
padding-top: v-bind(headerHeight);
|
||||
}
|
||||
.tab-height {
|
||||
height: v-bind(tabHeight);
|
||||
}
|
||||
.header-tab_height {
|
||||
height: v-bind(headerAndTabHeight);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<h3>vertical-mix</h3>
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped></style>
|
@ -0,0 +1,38 @@
|
||||
<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 '@/hooks';
|
||||
|
||||
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>
|
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<n-layout-footer>
|
||||
<div class="footer-height flex-center">Copyright ©2021 Soybean Admin</div>
|
||||
</n-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { NLayoutFooter } from 'naive-ui';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const footerHeight = computed(() => `${theme.footerStyle.height}px`);
|
||||
</script>
|
||||
<style scoped>
|
||||
.footer-height {
|
||||
height: v-bind(footerHeight);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,14 @@
|
||||
<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>
|
@ -0,0 +1,12 @@
|
||||
<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>
|
@ -0,0 +1,91 @@
|
||||
<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 { EnumRoutePath } from '@/enum';
|
||||
import { useThemeStore } from '@/store';
|
||||
import type { RoutePathKey } from '@/interface';
|
||||
|
||||
type Breadcrumb = DropdownOption & {
|
||||
key: string;
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
routeName: RoutePathKey;
|
||||
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 RoutePathKey;
|
||||
const breadcrumItem: Breadcrumb = {
|
||||
key: routeName,
|
||||
label: (item.meta?.title as string) || '',
|
||||
disabled: item.path === EnumRoutePath.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 RoutePathKey;
|
||||
router.push({ name: key });
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,27 @@
|
||||
<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>
|
@ -0,0 +1,15 @@
|
||||
<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>
|
@ -0,0 +1,13 @@
|
||||
<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>
|
@ -0,0 +1,15 @@
|
||||
<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>
|
@ -0,0 +1,55 @@
|
||||
<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 { useRouterChange } from '@/hooks';
|
||||
import { iconifyRender, resetAuthStorage } from '@/utils';
|
||||
import avatar from '@/assets/svg/avatar/avatar01.svg';
|
||||
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
const { toLogin } = useRouterChange();
|
||||
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>
|
@ -0,0 +1,19 @@
|
||||
import HeaderMenu from './HeaderMenu.vue';
|
||||
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 GithubSite from './GithubSite.vue';
|
||||
|
||||
export {
|
||||
HeaderMenu,
|
||||
GlobalBreadcrumb,
|
||||
UserAvatar,
|
||||
MenuCollapse,
|
||||
ThemeMode,
|
||||
FullScreen,
|
||||
SettingDrawerButton,
|
||||
GithubSite
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="global-header flex-y-center w-full">
|
||||
<global-logo v-if="showLogo" :show-title="true" class="h-full" :style="{ width: theme.menuStyle.width + 'px' }" />
|
||||
<div class="flex-1-hidden flex-y-center h-full">
|
||||
<menu-collapse v-if="showMenuCollape" />
|
||||
<global-breadcrumb v-if="theme.crumbsStyle.visible" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showMenu"
|
||||
class="flex-1-hidden flex-y-center h-full"
|
||||
:style="{ justifyContent: theme.menuStyle.horizontalPosition }"
|
||||
>
|
||||
<header-menu />
|
||||
</div>
|
||||
<div class="flex justify-end h-full">
|
||||
<github-site />
|
||||
<full-screen />
|
||||
<theme-mode />
|
||||
<user-avatar />
|
||||
<setting-drawer-button v-if="showSettingButton" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store';
|
||||
import {
|
||||
HeaderMenu,
|
||||
GlobalBreadcrumb,
|
||||
UserAvatar,
|
||||
MenuCollapse,
|
||||
ThemeMode,
|
||||
FullScreen,
|
||||
GithubSite,
|
||||
SettingDrawerButton
|
||||
} from './components';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
|
||||
interface Props {
|
||||
/** 显示logo */
|
||||
showLogo: boolean;
|
||||
/** 显示菜单折叠按钮 */
|
||||
showMenuCollape: boolean;
|
||||
/** 显示菜单 */
|
||||
showMenu: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const showSettingButton = import.meta.env.DEV || import.meta.env.VITE_HTTP_ENV === 'STAGING';
|
||||
</script>
|
||||
<style scoped>
|
||||
.global-header {
|
||||
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<a href="/" class="flex-center nowrap-hidden bg-light dark:bg-dark cursor-pointer">
|
||||
<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>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { SystemLogo } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { useAppTitle } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
/** 显示名字 */
|
||||
showTitle: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
const title = useAppTitle();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { useThemeStore, useAppStore } from '@/store';
|
||||
import { menus } from '@/router';
|
||||
import { GlobalMenuOption } from '@/interface';
|
||||
|
||||
const theme = useThemeStore();
|
||||
const app = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
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>
|
@ -0,0 +1,73 @@
|
||||
<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>
|
@ -0,0 +1,35 @@
|
||||
<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>
|
@ -0,0 +1,153 @@
|
||||
<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>
|
@ -0,0 +1,3 @@
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
|
||||
export { ContextMenu };
|
@ -0,0 +1,4 @@
|
||||
import MultiTab from './MultiTab/index.vue';
|
||||
import ReloadButton from './ReloadButton/index.vue';
|
||||
|
||||
export { MultiTab, ReloadButton };
|
41
src/layouts/BaseLayout/components/common/GlobalTab/index.vue
Normal file
41
src/layouts/BaseLayout/components/common/GlobalTab/index.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="multi-tab flex-center w-full pl-16px bg-light dark:bg-dark">
|
||||
<div class="flex-1-hidden h-full">
|
||||
<better-scroll :options="{ scrollX: true, scrollY: false, click: true }">
|
||||
<multi-tab />
|
||||
</better-scroll>
|
||||
</div>
|
||||
<reload-button />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
import { BetterScroll } from '@/components';
|
||||
import { MultiTab, ReloadButton } from './components';
|
||||
|
||||
const route = useRoute();
|
||||
const { initMultiTab, addMultiTab, setActiveMultiTab } = useAppStore();
|
||||
|
||||
function init() {
|
||||
initMultiTab();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
newValue => {
|
||||
addMultiTab(route);
|
||||
setActiveMultiTab(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
// 初始化
|
||||
init();
|
||||
</script>
|
||||
<style scoped>
|
||||
.multi-tab {
|
||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<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>
|
@ -0,0 +1,59 @@
|
||||
<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>
|
@ -0,0 +1,3 @@
|
||||
import NavType from './NavType.vue';
|
||||
|
||||
export { NavType };
|
@ -0,0 +1,37 @@
|
||||
<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>
|
@ -0,0 +1,81 @@
|
||||
<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>
|
@ -0,0 +1,52 @@
|
||||
<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>
|
@ -0,0 +1,18 @@
|
||||
<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>
|
@ -0,0 +1,65 @@
|
||||
<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>
|
@ -0,0 +1,26 @@
|
||||
<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>
|
@ -0,0 +1,16 @@
|
||||
<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>
|
@ -0,0 +1,4 @@
|
||||
import ColorBlock from './ColorBlock.vue';
|
||||
import SettingMenuItem from './SettingMenuItem.vue';
|
||||
|
||||
export { ColorBlock, SettingMenuItem };
|
@ -0,0 +1,8 @@
|
||||
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 };
|
@ -0,0 +1,21 @@
|
||||
<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>
|
9
src/layouts/BaseLayout/components/common/index.ts
Normal file
9
src/layouts/BaseLayout/components/common/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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 SettingDrawer from './SettingDrawer/index.vue';
|
||||
|
||||
export { GlobalHeader, GlobalContent, GlobalFooter, GlobalLogo, GlobalMenu, GlobalTab, SettingDrawer };
|
7
src/layouts/BaseLayout/components/index.ts
Normal file
7
src/layouts/BaseLayout/components/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
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';
|
25
src/layouts/BaseLayout/index.vue
Normal file
25
src/layouts/BaseLayout/index.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<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>
|
@ -6,6 +6,7 @@
|
||||
:collapsed="app.menu.collapsed"
|
||||
:collapsed-width="theme.menuStyle.collapsedWidth"
|
||||
:width="menuWidth"
|
||||
:native-scrollbar="false"
|
||||
@collapse="handleMenuCollapse(true)"
|
||||
@expand="handleMenuCollapse(false)"
|
||||
>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import BaseLayout from './BaseLayout/index.vue';
|
||||
import BasicLayout from './BasicLayout/index.vue';
|
||||
import BlankLayout from './BlankLayout/index.vue';
|
||||
import RouterViewLayout from './RouterViewLayout/index.vue';
|
||||
|
||||
export { BasicLayout, BlankLayout, RouterViewLayout };
|
||||
export { BaseLayout, BasicLayout, BlankLayout, RouterViewLayout };
|
||||
|
@ -76,6 +76,9 @@
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"footerStyle": {
|
||||
"height": 48
|
||||
},
|
||||
"pageStyle": {
|
||||
"animate": true,
|
||||
"animateType": "fade-slide",
|
||||
|
@ -67,6 +67,9 @@ const defaultThemeSettings: ThemeSettings = {
|
||||
visible: true,
|
||||
showIcon: true
|
||||
},
|
||||
footerStyle: {
|
||||
height: 48
|
||||
},
|
||||
pageStyle: {
|
||||
animate: true,
|
||||
animateType: 'fade-slide',
|
||||
|
@ -52,7 +52,8 @@ export default defineConfig({
|
||||
'error-pressed': 'var(--error-color-pressed)',
|
||||
'error-active': 'var(--error-color-active)',
|
||||
light: '#ffffff',
|
||||
dark: '#18181c'
|
||||
dark: '#18181c',
|
||||
'deep-dark': '#101014'
|
||||
},
|
||||
transitionProperty: ['width', 'height', 'background', 'background-color']
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user