mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-24 12:26:38 +08:00
fix(projects): 修复vertical-mix布局、重构初始化的loading
This commit is contained in:
parent
b2a4ddf5e3
commit
579e07400e
23
index.html
23
index.html
@ -7,20 +7,23 @@
|
||||
<title><%= appName %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="appProvider" style="display: none"></div>
|
||||
<div id="app">
|
||||
<div class="fixed-center flex-col">
|
||||
<div id="loadingLogo" class="w-128px h-128px text-primary"></div>
|
||||
<div class="w-56px h-56px my-36px">
|
||||
<div class="relative h-full animate-spin">
|
||||
<div class="absolute-lt init-loading-spin"></div>
|
||||
<div class="absolute-lb init-loading-spin animate-delay-500"></div>
|
||||
<div class="absolute-rt init-loading-spin animate-delay-1000"></div>
|
||||
<div class="absolute-rb init-loading-spin animate-delay-1500"></div>
|
||||
<div class="loading-container">
|
||||
<div id="loadingLogo" class="loading-svg"></div>
|
||||
<div class="loading-spin__container">
|
||||
<div class="loading-spin">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-28px font-medium text-[#646464]"><%= appTitle %></h2>
|
||||
<h2 class="loading-title"><%= appTitle %></h2>
|
||||
</div>
|
||||
<style>
|
||||
@import '/resource/loading.css';
|
||||
</style>
|
||||
<script src="/resource/loading.js"></script>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
91
public/resource/loading.css
Normal file
91
public/resource/loading.css
Normal file
@ -0,0 +1,91 @@
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-svg {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-spin__container {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 36px 0;
|
||||
}
|
||||
|
||||
.loading-spin {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
animation: loadingSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0;
|
||||
}
|
||||
.right-0 {
|
||||
right: 0;
|
||||
}
|
||||
.top-0 {
|
||||
top: 0;
|
||||
}
|
||||
.bottom-0 {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.loading-spin-item {
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes loadingSpin {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-delay-500 {
|
||||
-webkit-animation-delay: 500ms;
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
.loading-delay-1000 {
|
||||
-webkit-animation-delay: 1000ms;
|
||||
animation-delay: 1000ms;
|
||||
}
|
||||
.loading-delay-1500 {
|
||||
-webkit-animation-delay: 1500ms;
|
||||
animation-delay: 1500ms;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #646464;
|
||||
}
|
44
public/resource/loading.js
Normal file
44
public/resource/loading.js
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 初始化加载效果的svg格式logo
|
||||
* @param { string }id - 元素id
|
||||
*/
|
||||
function initSvgLogo(id) {
|
||||
const svgStr = `<svg width="128px" height="128px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 158.9 158.9" style="enable-background:new 0 0 158.9 158.9;" xml:space="preserve">
|
||||
<path style="fill:none" d="M0,158.9C0,106.3,0,53.7,0,1.1C0,0.2,0.2,0,1.1,0c52.2,0,104.5,0,156.7,0c0.9,0,1.1,0.2,1.1,1.1
|
||||
c0,52.2,0,104.5,0,156.7c0,0.9-0.2,1.1-1.1,1.1C105.2,158.8,52.6,158.8,0,158.9z" />
|
||||
<path style="fill:currentColor" d="M81.3,55.9c-0.1-11.7-2.9-22.5-9.4-32.4c-1-1.5-2.1-2.9-2.5-4.7c-0.7-3.4,0.9-6.9,4-8.6c3-1.7,6.8-1.2,9.3,1.2
|
||||
c2.4,2.6,4.4,5.6,5.9,8.8c4.7,8.9,7.6,18.6,8.4,28.6c1,12.5-0.7,25-5.2,36.7c-0.9,2.5-1.9,4.9-3,7.3c-0.3,0.4-0.3,1,0,1.4
|
||||
c9.6,13.3,21.8,23,37.8,27.2c6.4,1.7,13.1,2.3,19.7,1.6c4.2-0.4,7.9,2.7,8.4,6.9c0.7,4.3-2.3,8.3-6.6,9c0,0,0,0-0.1,0
|
||||
c-7.7,0.9-15.5,0.5-23-1.3c-13.9-3.1-26.7-10-36.9-19.9c-4.4-4.2-8.4-8.8-11.9-13.7c-0.5-0.8-1.4-1.2-2.3-1.1
|
||||
c-9.5,0.7-18.8,3.3-27.4,7.6c-11.6,6-20.7,14.6-26.4,26.4c-0.7,1.9-2,3.5-3.7,4.7c-2.9,1.7-6.6,1.5-9.2-0.7c-2.8-2.2-3.8-6-2.4-9.3
|
||||
c2.2-5.2,5.1-10.1,8.7-14.5c12.2-15.4,28.2-24.6,47.3-28.6c4-0.8,8.1-1.4,12.2-1.6c0.5,0,1-0.3,1.2-0.8c3.3-7.1,5.5-14.6,6.5-22.3
|
||||
C81.1,61.2,81.3,58.6,81.3,55.9z" />
|
||||
<path style="fill:currentColor" d="M136.3,108.3c-3.8-0.5-7.6-1.4-11.1-2.9c-7.7-2.8-14.4-7.5-19.7-13.8c-2.9-3.3-2.5-8.4,0.8-11.3
|
||||
c1.4-1.2,3.1-1.9,4.9-1.9c2.5-0.1,5,1,6.5,2.9c4.9,5.6,11.6,9.4,18.9,10.8c1.5,0.2,3.1,0.6,4.5,1.2c3.2,1.8,4.8,5.6,3.8,9.2
|
||||
C144,106.1,140.8,108.4,136.3,108.3z" />
|
||||
<path style="fill:currentColor" d="M55.7,33.3c3,0.2,5.6,2.2,6.6,5c2.2,5.4,3.4,11.2,3.6,17c0.3,5.9-0.6,11.7-2.5,17.3c-2,5.8-8.2,7.8-12.9,4.2
|
||||
c-2.6-2.2-3.6-5.8-2.4-9c1.4-4,1.9-8.2,1.7-12.4c-0.2-3.8-1-7.5-2.4-11C45.3,38.9,49.2,33.3,55.7,33.3z" />
|
||||
<path style="fill:currentColor" d="M77.9,126.6c0,3.9-2.8,7.2-6.7,7.9c-7.8,1.5-14.8,5.9-19.7,12.2c-2.7,3.5-7.6,4.2-11.2,1.6
|
||||
c-3.6-2.6-4.3-7.6-1.7-11.2c0.1-0.1,0.2-0.3,0.3-0.4c4.1-5.2,9.3-9.6,15.1-12.8c4.4-2.5,9.1-4.2,14-5.1
|
||||
C73.3,117.7,77.9,121.3,77.9,126.6z" />
|
||||
</svg>
|
||||
`;
|
||||
const appEl = document.querySelector(id);
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = svgStr;
|
||||
if (appEl) {
|
||||
appEl.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function addThemeColorCssVars() {
|
||||
const key = '__THEME_COLOR__';
|
||||
const themeColor = '#1890ff';
|
||||
const cssVars = window.localStorage.getItem(key) || `--primary-color: ${themeColor}`;
|
||||
document.documentElement.style.cssText = cssVars;
|
||||
}
|
||||
|
||||
initSvgLogo('#loadingLogo');
|
||||
|
||||
addThemeColorCssVars();
|
25
src/App.vue
25
src/App.vue
@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<app-provider>
|
||||
<n-config-provider
|
||||
:theme="theme.naiveTheme"
|
||||
:theme-overrides="theme.naiveThemeOverrides"
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
class="h-full"
|
||||
>
|
||||
<naive-provider>
|
||||
<router-view />
|
||||
</app-provider>
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { subscribeStore } from '@/store';
|
||||
import { useTheme } from '@/composables';
|
||||
import AppProvider from './AppProvider.vue';
|
||||
import { NConfigProvider, zhCN, dateZhCN } from 'naive-ui';
|
||||
import { NaiveProvider } from '@/components';
|
||||
import { useThemeStore, subscribeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
function init() {
|
||||
subscribeStore();
|
||||
useTheme();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<n-config-provider
|
||||
:theme="theme.naiveTheme"
|
||||
:theme-overrides="theme.naiveThemeOverrides"
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
class="h-full"
|
||||
>
|
||||
<naive-provider>
|
||||
<slot></slot>
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NConfigProvider, zhCN, dateZhCN } from 'naive-ui';
|
||||
import { NaiveProvider } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,4 +1,3 @@
|
||||
export * from './system';
|
||||
export * from './router';
|
||||
export * from './theme';
|
||||
export * from './layout';
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import { useOsTheme } from 'naive-ui';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
export function useTheme() {
|
||||
const osTheme = useOsTheme();
|
||||
const theme = useThemeStore();
|
||||
const { width } = useElementSize(document.documentElement);
|
||||
|
||||
/** 监听操作系统主题模式 */
|
||||
const stopHandle = watch(
|
||||
osTheme,
|
||||
newValue => {
|
||||
const isDark = newValue === 'dark';
|
||||
theme.setDarkMode(isDark);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 禁用横向滚动
|
||||
* @description 页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止
|
||||
*/
|
||||
const anotherStopHandle = watch(width, newValue => {
|
||||
if (newValue < theme.layout.minWidth) {
|
||||
document.documentElement.style.overflowX = 'auto';
|
||||
} else {
|
||||
document.documentElement.style.overflowX = 'hidden';
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
anotherStopHandle();
|
||||
});
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export enum EnumStorageKey {
|
||||
/** 主题颜色 */
|
||||
'theme-color' = '__THEME_COLOR__',
|
||||
/** 用户token */
|
||||
'token' = '__TOKEN__',
|
||||
/** 用户刷新token */
|
||||
|
3
src/interface/expose.ts
Normal file
3
src/interface/expose.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface ExposeLayoutMixMenu {
|
||||
resetFirstDegreeMenus(): void;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export * from './enum';
|
||||
export * from './theme';
|
||||
export * from './system';
|
||||
export * from './expose';
|
||||
export * from './layout';
|
||||
|
@ -4,7 +4,11 @@
|
||||
<n-breadcrumb-item>
|
||||
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
||||
<span>
|
||||
<component :is="breadcrumb.icon" v-if="theme.header.crumb.showIcon" class="inline-block mr-4px text-16px" />
|
||||
<component
|
||||
:is="breadcrumb.icon"
|
||||
v-if="theme.header.crumb.showIcon"
|
||||
class="inline-block align-text-bottom mr-4px text-16px"
|
||||
/>
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
</span>
|
||||
</n-dropdown>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<n-button :text="true" class="h-36px" @click="app.toggleSiderCollapse">
|
||||
<icon-ph-caret-double-right-bold v-if="app.siderCollapse" class="text-16px" />
|
||||
<icon-ph-caret-double-left-bold v-else class="text-16px" />
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NButton } from 'naive-ui';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,45 @@
|
||||
<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-300 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;
|
||||
/** 当前激活状态的理由名称 */
|
||||
activeRouteName: string;
|
||||
/** 路由图标 */
|
||||
icon?: () => VNodeChild;
|
||||
/** mini尺寸的路由 */
|
||||
isMini?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: undefined,
|
||||
isMini: false
|
||||
});
|
||||
|
||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||
|
||||
const isActive = computed(() => props.routeName === props.activeRouteName);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full transition-width duration-300 ease-in-out"
|
||||
:style="{ width: app.mixSiderFixed ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||
>
|
||||
<dark-mode-container
|
||||
class="drawer-shadow absolute-lt flex-col-stretch h-full nowrap-hidden"
|
||||
:style="{ width: showDrawer ? theme.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||
>
|
||||
<header class="header-height flex-y-center justify-between" :style="{ height: theme.header.height + 'px' }">
|
||||
<h2 class="text-primary pl-8px text-16px font-bold">{{ title }}</h2>
|
||||
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="app.toggleMixSiderFixed">
|
||||
<icon-mdi-pin-off v-if="app.mixSiderFixed" />
|
||||
<icon-mdi-pin v-else />
|
||||
</div>
|
||||
</header>
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menus"
|
||||
:expanded-keys="expandedKeys"
|
||||
:indent="18"
|
||||
@update:value="handleUpdateMenu"
|
||||
@update:expanded-keys="handleUpdateExpandedKeys"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</dark-mode-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NScrollbar, NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import { useAppInfo, useRouterPush } from '@/composables';
|
||||
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
|
||||
interface Props {
|
||||
/** 菜单抽屉可见性 */
|
||||
visible: boolean;
|
||||
/** 子菜单数据 */
|
||||
menus: GlobalMenuOption[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const route = useRoute();
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const showDrawer = computed(() => (props.visible && props.menus.length) || app.mixSiderFixed);
|
||||
const { title } = useAppInfo();
|
||||
|
||||
const activeKey = computed(() => route.name as string);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
}
|
||||
|
||||
function handleUpdateExpandedKeys(keys: string[]) {
|
||||
expandedKeys.value = keys;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
expandedKeys.value = getActiveKeyPathsOfMenus(activeKey.value, props.menus);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
.drawer-shadow {
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,5 @@
|
||||
import MixMenuDetail from './MixMenuDetail.vue';
|
||||
import MixMenuDrawer from './MixMenuDrawer.vue';
|
||||
import MixMenuCollapse from './MixMenuCollapse.vue';
|
||||
|
||||
export { MixMenuDetail, MixMenuDrawer, MixMenuCollapse };
|
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<dark-mode-container class="flex h-full" @mouseleave="resetFirstDegreeMenus">
|
||||
<div class="flex-1 flex-col-stretch h-full">
|
||||
<global-logo :show-title="false" :style="{ height: theme.header.height + 'px' }" />
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<mix-menu-detail
|
||||
v-for="item in firstDegreeMenus"
|
||||
:key="item.routeName"
|
||||
:route-name="item.routeName"
|
||||
:active-route-name="activeParentRouteName"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:is-mini="app.siderCollapse"
|
||||
@click="handleMixMenu(item.routeName, item.hasChildren)"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
<mix-menu-collapse />
|
||||
</div>
|
||||
<mix-menu-drawer :visible="drawerVisible" :menus="activeChildMenus" />
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NScrollbar } from 'naive-ui';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { GlobalLogo } from '@/layouts/common';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
import { MixMenuDetail, MixMenuDrawer, MixMenuCollapse } from './components';
|
||||
|
||||
const route = useRoute();
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: hideDrawer } = useBoolean();
|
||||
|
||||
const activeParentRouteName = ref('');
|
||||
function setActiveParentRouteName(routeName: string) {
|
||||
activeParentRouteName.value = routeName;
|
||||
}
|
||||
|
||||
const firstDegreeMenus = computed(() =>
|
||||
routeStore.menus.map(item => {
|
||||
const { routeName, label } = item;
|
||||
const icon = item?.icon;
|
||||
const hasChildren = Boolean(item.children && item.children.length);
|
||||
|
||||
return {
|
||||
routeName,
|
||||
label,
|
||||
icon,
|
||||
hasChildren
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
function getActiveParentRouteName() {
|
||||
firstDegreeMenus.value.some(item => {
|
||||
const routeName = route.name as string;
|
||||
const flag = routeName?.includes(item.routeName);
|
||||
if (flag) {
|
||||
setActiveParentRouteName(item.routeName);
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
}
|
||||
|
||||
function handleMixMenu(routeName: string, hasChildren: boolean) {
|
||||
setActiveParentRouteName(routeName);
|
||||
if (hasChildren) {
|
||||
openDrawer();
|
||||
} else {
|
||||
routerPush({ name: routeName });
|
||||
}
|
||||
}
|
||||
|
||||
function resetFirstDegreeMenus() {
|
||||
getActiveParentRouteName();
|
||||
hideDrawer();
|
||||
}
|
||||
|
||||
const activeChildMenus = computed(() => {
|
||||
const menus: GlobalMenuOption[] = [];
|
||||
routeStore.menus.some(item => {
|
||||
const flag = item.routeName === activeParentRouteName.value && Boolean(item.children?.length);
|
||||
if (flag) {
|
||||
menus.push(...item.children!);
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
return menus;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
getActiveParentRouteName();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<n-scrollbar>
|
||||
<n-scrollbar class="flex-1-hidden">
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:collapsed="app.siderCollapse"
|
||||
:collapsed-width="theme.sider.collapsedWidth"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menus"
|
||||
:options="routeStore.menus"
|
||||
:expanded-keys="expandedKeys"
|
||||
:indent="18"
|
||||
@update:value="handleUpdateMenu"
|
||||
@ -21,6 +21,7 @@ import { NScrollbar, NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
|
||||
const route = useRoute();
|
||||
@ -29,26 +30,9 @@ const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
|
||||
const activeKey = computed(() => route.name as string);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function getExpendedKeys() {
|
||||
const keys = menus.value.map(menu => getActiveKeysInMenus(menu)).flat();
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getActiveKeysInMenus(menu: GlobalMenuOption) {
|
||||
const keys: string[] = [];
|
||||
if (activeKey.value.includes(menu.routeName)) {
|
||||
keys.push(menu.routeName);
|
||||
}
|
||||
if (menu.children) {
|
||||
keys.push(...menu.children.map(item => getActiveKeysInMenus(item as GlobalMenuOption)).flat(1));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
@ -61,7 +45,7 @@ function handleUpdateExpandedKeys(keys: string[]) {
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
expandedKeys.value = getExpendedKeys();
|
||||
expandedKeys.value = getActiveKeyPathsOfMenus(activeKey.value, routeStore.menus);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
import VerticalMenu from './VerticalMenu.vue';
|
||||
|
||||
export { VerticalMenu };
|
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<dark-mode-container class="flex-col-stretch h-full">
|
||||
<global-logo v-if="!isHorizontalMix" :show-title="showTitle" :style="{ height: theme.header.height + 'px' }" />
|
||||
<vertical-menu />
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import { GlobalLogo } from '@/layouts/common';
|
||||
import { VerticalMenu } from './components';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isHorizontalMix = computed(() => theme.layout.mode === 'horizontal-mix');
|
||||
const showTitle = computed(() => !app.siderCollapse && theme.layout.mode !== 'vertical-mix');
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,3 +1,4 @@
|
||||
import SiderMenu from './SiderMenu.vue';
|
||||
import VerticalSider from './VerticalSider/index.vue';
|
||||
import VerticalMixSider from './VerticalMixSider/index.vue';
|
||||
|
||||
export { SiderMenu };
|
||||
export { VerticalSider, VerticalMixSider };
|
||||
|
@ -1,18 +1,16 @@
|
||||
<template>
|
||||
<dark-mode-container class="global-sider flex-col-stretch h-full">
|
||||
<global-logo :show-title="!app.siderCollapse" :style="{ height: theme.header.height + 'px' }" />
|
||||
<sider-menu class="flex-1-hidden" />
|
||||
</dark-mode-container>
|
||||
<vertical-sider v-if="!isVerticalMix" class="global-sider" />
|
||||
<vertical-mix-sider v-else class="global-sider" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
import { SiderMenu } from './components';
|
||||
import { computed } from 'vue';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { VerticalSider, VerticalMixSider } from './components';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix');
|
||||
</script>
|
||||
<style scoped>
|
||||
.global-sider {
|
||||
|
13
src/main.ts
13
src/main.ts
@ -1,23 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
import { setupAssets, setupInitSvgLogo } from '@/plugins';
|
||||
import { setupAssets } from '@/plugins';
|
||||
import { setupRouter } from '@/router';
|
||||
import { setupStore } from '@/store';
|
||||
import AppProvider from './AppProvider.vue';
|
||||
import App from './App.vue';
|
||||
|
||||
async function setupApp() {
|
||||
// 初始化加载的svg logo
|
||||
setupInitSvgLogo('#loadingLogo');
|
||||
|
||||
// 引入静态资源
|
||||
setupAssets();
|
||||
|
||||
// 挂载 appProvider 解决路由守卫,Axios中可使用,LoadingBar,Dialog,Message 等之类组件
|
||||
const appProvider = createApp(AppProvider);
|
||||
setupStore(appProvider);
|
||||
appProvider.mount('#appProvider');
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 挂载pinia状态
|
||||
setupStore(app);
|
||||
|
||||
// 挂载路由
|
||||
|
@ -58,6 +58,10 @@ export const useAppStore = defineStore('app-store', {
|
||||
/** 设置 vertical-mix模式下 侧边栏的固定状态 */
|
||||
setMixSiderIsFixed(isFixed: boolean) {
|
||||
this.mixSiderFixed = isFixed;
|
||||
},
|
||||
/** 设置 vertical-mix模式下 侧边栏的固定状态 */
|
||||
toggleMixSiderFixed() {
|
||||
this.mixSiderFixed = !this.mixSiderFixed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,102 +0,0 @@
|
||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||
import { kebabCase } from 'lodash-es';
|
||||
import { getColorPalette, addColorAlpha } from '@/utils';
|
||||
|
||||
type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error';
|
||||
type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';
|
||||
type ColorKey = `${ColorType}Color${ColorScene}`;
|
||||
type ThemeColor = {
|
||||
[key in ColorKey]?: string;
|
||||
};
|
||||
interface ColorAction {
|
||||
scene: ColorScene;
|
||||
handler: (color: string) => string;
|
||||
}
|
||||
|
||||
/** 获取主题颜色的各种场景对应的颜色 */
|
||||
function getThemeColors(colors: [ColorType, string][]) {
|
||||
const colorActions: ColorAction[] = [
|
||||
{ scene: '', handler: color => color },
|
||||
{ scene: 'Suppl', handler: color => color },
|
||||
{ scene: 'Hover', handler: color => getColorPalette(color, 5) },
|
||||
{ scene: 'Pressed', handler: color => getColorPalette(color, 7) },
|
||||
{ scene: 'Active', handler: color => addColorAlpha(color, 0.1) }
|
||||
];
|
||||
|
||||
const themeColor: ThemeColor = {};
|
||||
|
||||
colors.forEach(color => {
|
||||
colorActions.forEach(action => {
|
||||
const [colorType, colorValue] = color;
|
||||
const colorKey: ColorKey = `${colorType}Color${action.scene}`;
|
||||
themeColor[colorKey] = action.handler(colorValue);
|
||||
});
|
||||
});
|
||||
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
/** 获取naive的主题颜色 */
|
||||
export function getNaiveThemeOverrides(colors: { [key in ColorType]: string }): GlobalThemeOverrides {
|
||||
const { primary, info, success, warning, error } = colors;
|
||||
const themeColors = getThemeColors([
|
||||
['primary', primary],
|
||||
['info', info],
|
||||
['success', success],
|
||||
['warning', warning],
|
||||
['error', error]
|
||||
]);
|
||||
|
||||
const colorLoading = primary;
|
||||
|
||||
return {
|
||||
common: {
|
||||
...themeColors
|
||||
},
|
||||
LoadingBar: {
|
||||
colorLoading
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type ThemeVars = Exclude<GlobalThemeOverrides['common'], undefined>;
|
||||
type ThemeVarsKeys = keyof ThemeVars;
|
||||
|
||||
/** 添加css vars至html */
|
||||
export function addThemeCssVarsToHtml(themeVars: ThemeVars, action: 'add' | 'update' = 'add') {
|
||||
const keys = Object.keys(themeVars) as ThemeVarsKeys[];
|
||||
const style: string[] = [];
|
||||
keys.forEach(key => {
|
||||
style.push(`--${kebabCase(key)}: ${themeVars[key]}`);
|
||||
});
|
||||
const styleStr = style.join(';');
|
||||
if (action === 'add') {
|
||||
document.documentElement.style.cssText = styleStr;
|
||||
} else {
|
||||
document.documentElement.style.cssText += styleStr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主题颜色更新css vars
|
||||
* @param primaryColor
|
||||
*/
|
||||
export function updateThemeCssVarsByPrimary(primaryColor: string) {
|
||||
const themeColor = getThemeColors([['primary', primaryColor]]);
|
||||
addThemeCssVarsToHtml(themeColor, 'update');
|
||||
}
|
||||
|
||||
/** windicss 暗黑模式 */
|
||||
export function handleWindicssDarkMode() {
|
||||
const DARK_CLASS = 'dark';
|
||||
function addDarkClass() {
|
||||
document.documentElement.classList.add(DARK_CLASS);
|
||||
}
|
||||
function removeDarkClass() {
|
||||
document.documentElement.classList.remove(DARK_CLASS);
|
||||
}
|
||||
return {
|
||||
addDarkClass,
|
||||
removeDarkClass
|
||||
};
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import { useOsTheme } from 'naive-ui';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { objectAssign } from '@/utils';
|
||||
import type { ThemeSetting, ThemeLayoutMode, ThemeTabMode, ThemeAnimateMode } from '@/interface';
|
||||
import { handleWindicssDarkMode, updateThemeCssVarsByPrimary } from './helpers';
|
||||
|
||||
export interface LayoutFunc {
|
||||
/** 设置布局最小宽度 */
|
||||
setLayoutMinWidth(minWidth: number): void;
|
||||
/** 设置布局模式 */
|
||||
setLayoutMode(mode: ThemeLayoutMode): void;
|
||||
}
|
||||
export function useLayoutFunc(layout: ThemeSetting['layout']): LayoutFunc {
|
||||
function setLayout(data: Partial<ThemeSetting['layout']>) {
|
||||
objectAssign(layout, data);
|
||||
}
|
||||
|
||||
function setLayoutMinWidth(minWidth: number) {
|
||||
setLayout({ minWidth });
|
||||
}
|
||||
function setLayoutMode(mode: ThemeLayoutMode) {
|
||||
setLayout({ mode });
|
||||
}
|
||||
|
||||
return {
|
||||
setLayoutMinWidth,
|
||||
setLayoutMode
|
||||
};
|
||||
}
|
||||
|
||||
export interface HeaderFunc {
|
||||
/** 设置头部高度 */
|
||||
setHeaderHeight(height: number): void;
|
||||
/** 设置头部面包屑可见 */
|
||||
setHeaderCrumbVisible(visible: boolean): void;
|
||||
/** 设置头部面包屑图标可见 */
|
||||
setHeaderCrumbIconVisible(visible: boolean): void;
|
||||
}
|
||||
export function useHeaderFunc(header: ThemeSetting['header']): HeaderFunc {
|
||||
function setHeader(data: Partial<ThemeSetting['header']>) {
|
||||
objectAssign(header, data);
|
||||
}
|
||||
|
||||
function setHeaderHeight(height: number) {
|
||||
setHeader({ height });
|
||||
}
|
||||
function setHeaderCrumbVisible(visible: boolean) {
|
||||
setHeader({ crumb: { ...header.crumb, visible } });
|
||||
}
|
||||
function setHeaderCrumbIconVisible(visible: boolean) {
|
||||
setHeader({ crumb: { ...header.crumb, showIcon: visible } });
|
||||
}
|
||||
|
||||
return {
|
||||
setHeaderHeight,
|
||||
setHeaderCrumbVisible,
|
||||
setHeaderCrumbIconVisible
|
||||
};
|
||||
}
|
||||
|
||||
export interface TabFunc {
|
||||
/** 设置多页签可见 */
|
||||
setTabVisible(visible: boolean): void;
|
||||
/** 设置多页签高度 */
|
||||
setTabHeight(height: number): void;
|
||||
/** 设置多页签风格 */
|
||||
setTabMode(mode: ThemeTabMode): void;
|
||||
/** 设置多页签缓存 */
|
||||
setTabIsCache(isCache: boolean): void;
|
||||
}
|
||||
export function useTabFunc(tab: ThemeSetting['tab']): TabFunc {
|
||||
function setTab(data: Partial<ThemeSetting['tab']>) {
|
||||
objectAssign(tab, data);
|
||||
}
|
||||
|
||||
function setTabVisible(visible: boolean) {
|
||||
setTab({ visible });
|
||||
}
|
||||
function setTabHeight(height: number) {
|
||||
setTab({ height });
|
||||
}
|
||||
function setTabMode(mode: ThemeTabMode) {
|
||||
setTab({ mode });
|
||||
}
|
||||
function setTabIsCache(isCache: boolean) {
|
||||
setTab({ isCache });
|
||||
}
|
||||
|
||||
return {
|
||||
setTabVisible,
|
||||
setTabHeight,
|
||||
setTabMode,
|
||||
setTabIsCache
|
||||
};
|
||||
}
|
||||
|
||||
export interface SiderFunc {
|
||||
/** 侧边栏宽度 */
|
||||
setSiderWidth(width: number): void;
|
||||
/** 侧边栏折叠时的宽度 */
|
||||
setSiderCollapsedWidth(width: number): void;
|
||||
/** vertical-mix模式下侧边栏宽度 */
|
||||
setMixSiderWidth(width: number): void;
|
||||
/** vertical-mix模式下侧边栏折叠时的宽度 */
|
||||
setMixSiderCollapsedWidth(width: number): void;
|
||||
/** vertical-mix模式下侧边栏展示子菜单的宽度 */
|
||||
setMixSiderChildMenuWidth(width: number): void;
|
||||
}
|
||||
export function useSiderFunc(sider: ThemeSetting['sider']): SiderFunc {
|
||||
function setSider(data: Partial<ThemeSetting['sider']>) {
|
||||
objectAssign(sider, data);
|
||||
}
|
||||
|
||||
function setSiderWidth(width: number) {
|
||||
setSider({ width });
|
||||
}
|
||||
function setSiderCollapsedWidth(width: number) {
|
||||
setSider({ collapsedWidth: width });
|
||||
}
|
||||
function setMixSiderWidth(width: number) {
|
||||
setSider({ mixWidth: width });
|
||||
}
|
||||
function setMixSiderCollapsedWidth(width: number) {
|
||||
setSider({ mixCollapsedWidth: width });
|
||||
}
|
||||
function setMixSiderChildMenuWidth(width: number) {
|
||||
setSider({ mixChildMenuWidth: width });
|
||||
}
|
||||
|
||||
return {
|
||||
setSiderWidth,
|
||||
setSiderCollapsedWidth,
|
||||
setMixSiderWidth,
|
||||
setMixSiderCollapsedWidth,
|
||||
setMixSiderChildMenuWidth
|
||||
};
|
||||
}
|
||||
|
||||
export interface FooterFunc {
|
||||
/** 设置底部是否固定 */
|
||||
setFooterIsFixed(isFixed: boolean): void;
|
||||
/** 设置底部高度 */
|
||||
setFooterHeight(height: number): void;
|
||||
}
|
||||
export function useFooterFunc(footer: ThemeSetting['footer']): FooterFunc {
|
||||
function setFooter(data: Partial<ThemeSetting['footer']>) {
|
||||
objectAssign(footer, data);
|
||||
}
|
||||
|
||||
function setFooterIsFixed(isFixed: boolean) {
|
||||
setFooter({ fixed: isFixed });
|
||||
}
|
||||
function setFooterHeight(height: number) {
|
||||
setFooter({ height });
|
||||
}
|
||||
|
||||
return {
|
||||
setFooterIsFixed,
|
||||
setFooterHeight
|
||||
};
|
||||
}
|
||||
|
||||
export interface PageFunc {
|
||||
/** 设置切换页面时是否过渡动画 */
|
||||
setPageIsAnimate(animate: boolean): void;
|
||||
/** 设置页面过渡动画类型 */
|
||||
setPageAnimateMode(mode: ThemeAnimateMode): void;
|
||||
}
|
||||
export function usePageFunc(page: ThemeSetting['page']): PageFunc {
|
||||
function setPage(data: Partial<ThemeSetting['page']>) {
|
||||
objectAssign(page, data);
|
||||
}
|
||||
|
||||
function setPageIsAnimate(animate: boolean) {
|
||||
setPage({ animate });
|
||||
}
|
||||
function setPageAnimateMode(mode: ThemeAnimateMode) {
|
||||
setPage({ animateMode: mode });
|
||||
}
|
||||
|
||||
return {
|
||||
setPageIsAnimate,
|
||||
setPageAnimateMode
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作系统主题模式变化的回调函数
|
||||
* @param isDark - 暗黑模式
|
||||
*/
|
||||
type OsThemeCallback = (isDark: boolean) => void;
|
||||
|
||||
/** 监听操作系统主题模式 */
|
||||
export function osThemeWatcher(callback: OsThemeCallback) {
|
||||
/** 操作系统暗黑主题 */
|
||||
const osTheme = useOsTheme();
|
||||
|
||||
const stopHandle = watch(
|
||||
osTheme,
|
||||
newValue => {
|
||||
const isDark = newValue === 'dark';
|
||||
callback(isDark);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
}
|
||||
|
||||
/** 应用windicss的暗黑模式 */
|
||||
export function setupWindicssDarkMode(darkMode: Ref<boolean>) {
|
||||
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
||||
|
||||
const stopHandle = watch(
|
||||
() => darkMode.value,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
addDarkClass();
|
||||
} else {
|
||||
removeDarkClass();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用横向滚动
|
||||
* @description 页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止
|
||||
*/
|
||||
export function setupHiddenScroll(minWidthOfLayout: ComputedRef<number>) {
|
||||
const { width } = useElementSize(document.documentElement);
|
||||
|
||||
const stopHandle = watch(width, newValue => {
|
||||
if (newValue < minWidthOfLayout.value) {
|
||||
document.documentElement.style.overflowX = 'auto';
|
||||
} else {
|
||||
document.documentElement.style.overflowX = 'hidden';
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听主题颜色的变化
|
||||
* @param themeColor
|
||||
*/
|
||||
export function themeColorWatcher(themeColor: Ref<string>) {
|
||||
const stopHandle = watch(themeColor, newValue => {
|
||||
updateThemeCssVarsByPrimary(newValue);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { darkTheme } from 'naive-ui';
|
||||
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
|
||||
import { themeSetting } from '@/settings';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { getColorPalette } from '@/utils';
|
||||
import type { ThemeSetting, ThemeHorizontalMenuPosition } from '@/interface';
|
||||
import { getNaiveThemeOverrides, addThemeCssVarsToHtml } from './helpers';
|
||||
import {
|
||||
useLayoutFunc,
|
||||
useHeaderFunc,
|
||||
useTabFunc,
|
||||
useSiderFunc,
|
||||
useFooterFunc,
|
||||
usePageFunc,
|
||||
osThemeWatcher,
|
||||
setupWindicssDarkMode,
|
||||
setupHiddenScroll,
|
||||
themeColorWatcher
|
||||
} from './hooks';
|
||||
import type { LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc } from './hooks';
|
||||
|
||||
type BuiltInGlobalTheme = Omit<Required<GlobalTheme>, 'InternalSelectMenu' | 'InternalSelection'>;
|
||||
|
||||
interface ThemeStore extends LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc {
|
||||
/** 暗黑模式 */
|
||||
darkMode: Ref<boolean>;
|
||||
/** 设置暗黑模式 */
|
||||
setDarkMode(dark: boolean): void;
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDarkMode(): void;
|
||||
/** 布局样式 */
|
||||
layout: ThemeSetting['layout'];
|
||||
/** 主题颜色 */
|
||||
themeColor: Ref<string>;
|
||||
/** 设置系统主题颜色 */
|
||||
setThemeColor(color: string): void;
|
||||
/** 主题颜色列表 */
|
||||
themeColorList: string[];
|
||||
/** 其他颜色 */
|
||||
otherColor: ComputedRef<ThemeSetting['otherColor']>;
|
||||
/** 固定头部和多页签 */
|
||||
fixedHeaderAndTab: Ref<boolean>;
|
||||
/** 设置固定头部和多页签 */
|
||||
setIsFixedHeaderAndTab(isFixed: boolean): void;
|
||||
/** 重载按钮可见 */
|
||||
reloadVisible: Ref<boolean>;
|
||||
/** 设置 显示/隐藏 重载按钮 */
|
||||
setReloadVisible(visible: boolean): void;
|
||||
/** 头部 */
|
||||
header: ThemeSetting['header'];
|
||||
/** 多页签 */
|
||||
tab: ThemeSetting['tab'];
|
||||
/** 侧边栏 */
|
||||
sider: ThemeSetting['sider'];
|
||||
/** 菜单 */
|
||||
menu: ThemeSetting['menu'];
|
||||
/** 设置水平模式的菜单的位置 */
|
||||
setHorizontalMenuPosition(posiiton: ThemeHorizontalMenuPosition): void;
|
||||
/** 底部 */
|
||||
footer: ThemeSetting['footer'];
|
||||
/** 页面 */
|
||||
page: ThemeSetting['page'];
|
||||
/** naiveUI的主题配置 */
|
||||
naiveThemeOverrides: ComputedRef<GlobalThemeOverrides>;
|
||||
/** naive-ui暗黑主题 */
|
||||
naiveTheme: ComputedRef<BuiltInGlobalTheme | undefined>;
|
||||
/** 重置状态 */
|
||||
resetThemeStore(): void;
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('theme-store', () => {
|
||||
// 暗黑模式
|
||||
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
|
||||
|
||||
// 布局
|
||||
const layout = reactive<ThemeSetting['layout']>({
|
||||
...themeSetting.layout
|
||||
});
|
||||
const { setLayoutMinWidth, setLayoutMode } = useLayoutFunc(layout);
|
||||
|
||||
// 主题色
|
||||
const themeColor = ref(themeSetting.themeColor);
|
||||
/** 设置系统主题颜色 */
|
||||
function setThemeColor(color: string) {
|
||||
themeColor.value = color;
|
||||
}
|
||||
const { themeColorList } = themeSetting;
|
||||
const otherColor = computed<ThemeSetting['otherColor']>(() => ({
|
||||
...themeSetting.otherColor,
|
||||
info: getColorPalette(themeColor.value, 7)
|
||||
}));
|
||||
|
||||
// 固定头部和多页签
|
||||
const { bool: fixedHeaderAndTab, setBool: setIsFixedHeaderAndTab } = useBoolean(themeSetting.fixedHeaderAndTab);
|
||||
|
||||
// 重载按钮
|
||||
const { bool: reloadVisible, setBool: setReloadVisible } = useBoolean(themeSetting.showReload);
|
||||
|
||||
// 头部
|
||||
const header = reactive<ThemeSetting['header']>({
|
||||
height: themeSetting.header.height,
|
||||
crumb: { ...themeSetting.header.crumb }
|
||||
});
|
||||
const { setHeaderHeight, setHeaderCrumbVisible, setHeaderCrumbIconVisible } = useHeaderFunc(header);
|
||||
|
||||
// 多页签
|
||||
const tab = reactive<ThemeSetting['tab']>({
|
||||
...themeSetting.tab
|
||||
});
|
||||
const { setTabVisible, setTabHeight, setTabMode, setTabIsCache } = useTabFunc(tab);
|
||||
|
||||
// 侧边栏
|
||||
const sider = reactive<ThemeSetting['sider']>({
|
||||
...themeSetting.sider
|
||||
});
|
||||
const {
|
||||
setSiderWidth,
|
||||
setSiderCollapsedWidth,
|
||||
setMixSiderWidth,
|
||||
setMixSiderCollapsedWidth,
|
||||
setMixSiderChildMenuWidth
|
||||
} = useSiderFunc(sider);
|
||||
|
||||
// 菜单
|
||||
const menu = reactive<ThemeSetting['menu']>({
|
||||
...themeSetting.menu
|
||||
});
|
||||
function setHorizontalMenuPosition(posiiton: ThemeHorizontalMenuPosition) {
|
||||
menu.horizontalPosition = posiiton;
|
||||
}
|
||||
|
||||
// 底部
|
||||
const footer = reactive<ThemeSetting['footer']>({
|
||||
...themeSetting.footer
|
||||
});
|
||||
const { setFooterIsFixed, setFooterHeight } = useFooterFunc(footer);
|
||||
|
||||
// 页面
|
||||
const page = reactive<ThemeSetting['page']>({
|
||||
...themeSetting.page
|
||||
});
|
||||
const { setPageIsAnimate, setPageAnimateMode } = usePageFunc(page);
|
||||
|
||||
// naive主题
|
||||
const naiveThemeOverrides = computed<GlobalThemeOverrides>(() =>
|
||||
getNaiveThemeOverrides({ primary: themeColor.value, ...otherColor.value })
|
||||
);
|
||||
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
|
||||
|
||||
/** 重置theme状态 */
|
||||
function resetThemeStore() {
|
||||
setDarkMode(false);
|
||||
}
|
||||
|
||||
/** 初始化css vars, 并添加至html */
|
||||
function initThemeCssVars() {
|
||||
const updatedThemeVars = { ...naiveThemeOverrides.value.common };
|
||||
addThemeCssVarsToHtml(updatedThemeVars);
|
||||
}
|
||||
|
||||
/** 系统主题适应操作系统 */
|
||||
function handleAdaptOsTheme() {
|
||||
osThemeWatcher(isDark => {
|
||||
if (isDark) {
|
||||
setDarkMode(true);
|
||||
} else {
|
||||
setDarkMode(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
initThemeCssVars();
|
||||
handleAdaptOsTheme();
|
||||
setupWindicssDarkMode(darkMode);
|
||||
setupHiddenScroll(computed(() => layout.minWidth));
|
||||
themeColorWatcher(themeColor);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
const themeStore: ThemeStore = {
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
toggleDarkMode,
|
||||
layout,
|
||||
setLayoutMinWidth,
|
||||
setLayoutMode,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
themeColorList,
|
||||
otherColor,
|
||||
fixedHeaderAndTab,
|
||||
setIsFixedHeaderAndTab,
|
||||
reloadVisible,
|
||||
setReloadVisible,
|
||||
header,
|
||||
setHeaderHeight,
|
||||
setHeaderCrumbVisible,
|
||||
setHeaderCrumbIconVisible,
|
||||
tab,
|
||||
setTabVisible,
|
||||
setTabHeight,
|
||||
setTabMode,
|
||||
setTabIsCache,
|
||||
sider,
|
||||
setSiderWidth,
|
||||
setSiderCollapsedWidth,
|
||||
setMixSiderWidth,
|
||||
setMixSiderCollapsedWidth,
|
||||
setMixSiderChildMenuWidth,
|
||||
menu,
|
||||
setHorizontalMenuPosition,
|
||||
footer,
|
||||
setFooterIsFixed,
|
||||
setFooterHeight,
|
||||
page,
|
||||
setPageIsAnimate,
|
||||
setPageAnimateMode,
|
||||
naiveThemeOverrides,
|
||||
naiveTheme,
|
||||
resetThemeStore
|
||||
};
|
||||
|
||||
return themeStore;
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import { useBodyScroll } from '@/hooks';
|
||||
import { useAppStore } from '../modules';
|
||||
|
||||
@ -6,8 +7,15 @@ export default function subscribeAppStore() {
|
||||
const app = useAppStore();
|
||||
const { scrollBodyHandler } = useBodyScroll();
|
||||
|
||||
app.$subscribe((_mutation, state) => {
|
||||
// 弹窗打开时禁止滚动条
|
||||
scrollBodyHandler(state.settingDrawerVisible);
|
||||
const stopHandle = watch(
|
||||
() => app.settingDrawerVisible,
|
||||
newValue => {
|
||||
scrollBodyHandler(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import subscribeAppStore from './app';
|
||||
import subscribeThemeStore from './theme';
|
||||
|
||||
/** 订阅状态 */
|
||||
export function subscribeStore() {
|
||||
subscribeAppStore();
|
||||
subscribeThemeStore();
|
||||
}
|
||||
|
@ -1,17 +1,60 @@
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import { useOsTheme } from 'naive-ui';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { EnumStorageKey } from '@/enum';
|
||||
import { useThemeStore } from '../modules';
|
||||
|
||||
/** 订阅app store */
|
||||
export default function subscribeAppStore() {
|
||||
/** 订阅theme store */
|
||||
export default function subscribeThemeStore() {
|
||||
const theme = useThemeStore();
|
||||
const osTheme = useOsTheme();
|
||||
const { width } = useElementSize(document.documentElement);
|
||||
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
||||
|
||||
theme.$subscribe((_mutation, state) => {
|
||||
const stopThemeColor = watch(
|
||||
() => theme.themeColor,
|
||||
newValue => {
|
||||
window.localStorage.setItem(EnumStorageKey['theme-color'], `--primary-color: ${newValue};`);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听暗黑模式
|
||||
if (state.darkMode) {
|
||||
const stopDarkMode = watch(
|
||||
() => theme.darkMode,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
addDarkClass();
|
||||
} else {
|
||||
removeDarkClass();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听操作系统主题模式
|
||||
const stopOsTheme = watch(
|
||||
osTheme,
|
||||
newValue => {
|
||||
const isDark = newValue === 'dark';
|
||||
theme.setDarkMode(isDark);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 禁用横向滚动(页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止)
|
||||
const stopWidth = watch(width, newValue => {
|
||||
if (newValue < theme.layout.minWidth) {
|
||||
document.documentElement.style.overflowX = 'auto';
|
||||
} else {
|
||||
document.documentElement.style.overflowX = 'hidden';
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopThemeColor();
|
||||
stopDarkMode();
|
||||
stopOsTheme();
|
||||
stopWidth();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,3 +49,24 @@ export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): GlobalMenuO
|
||||
|
||||
return globalMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前路由所在菜单数据的paths
|
||||
* @param activeKey - 当前路由的key
|
||||
* @param menus - 菜单数据
|
||||
*/
|
||||
export function getActiveKeyPathsOfMenus(activeKey: string, menus: GlobalMenuOption[]) {
|
||||
const keys = menus.map(menu => getActiveKeyPathsOfMenu(activeKey, menu)).flat(1);
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getActiveKeyPathsOfMenu(activeKey: string, menu: GlobalMenuOption) {
|
||||
const keys: string[] = [];
|
||||
if (activeKey.includes(menu.routeName)) {
|
||||
keys.push(menu.routeName);
|
||||
}
|
||||
if (menu.children) {
|
||||
keys.push(...menu.children.map(item => getActiveKeyPathsOfMenu(activeKey, item)).flat(1));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
@ -12,13 +12,13 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"~/*": ["./*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": ["vite.config.*", "src/typings/*.d.ts", "src/**/*", "src/**/*.vue", "mock/**/*.ts", "build/**/*.ts", ".env-config.ts"],
|
||||
"exclude": ["/dist/**", "node_modules"]
|
||||
|
@ -40,9 +40,7 @@ export default defineConfig({
|
||||
'fixed-center': 'fixed left-0 top-0 flex-center wh-full',
|
||||
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
||||
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
||||
'transition-base': 'transition-all duration-300 ease-in-out',
|
||||
// 'dark-transition': "",
|
||||
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
|
||||
'transition-base': 'transition-all duration-300 ease-in-out'
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
|
Loading…
Reference in New Issue
Block a user