mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
fix(projects): 修复vertical-mix布局、重构初始化的loading
This commit is contained in:
23
index.html
23
index.html
@ -7,20 +7,23 @@
|
|||||||
<title><%= appName %></title>
|
<title><%= appName %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="appProvider" style="display: none"></div>
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="fixed-center flex-col">
|
<div class="loading-container">
|
||||||
<div id="loadingLogo" class="w-128px h-128px text-primary"></div>
|
<div id="loadingLogo" class="loading-svg"></div>
|
||||||
<div class="w-56px h-56px my-36px">
|
<div class="loading-spin__container">
|
||||||
<div class="relative h-full animate-spin">
|
<div class="loading-spin">
|
||||||
<div class="absolute-lt init-loading-spin"></div>
|
<div class="left-0 top-0 loading-spin-item"></div>
|
||||||
<div class="absolute-lb init-loading-spin animate-delay-500"></div>
|
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||||
<div class="absolute-rt init-loading-spin animate-delay-1000"></div>
|
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||||
<div class="absolute-rb init-loading-spin animate-delay-1500"></div>
|
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-28px font-medium text-[#646464]"><%= appTitle %></h2>
|
<h2 class="loading-title"><%= appTitle %></h2>
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
@import '/resource/loading.css';
|
||||||
|
</style>
|
||||||
|
<script src="/resource/loading.js"></script>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</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();
|
27
src/App.vue
27
src/App.vue
@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<app-provider>
|
<n-config-provider
|
||||||
<router-view />
|
:theme="theme.naiveTheme"
|
||||||
</app-provider>
|
:theme-overrides="theme.naiveThemeOverrides"
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
class="h-full"
|
||||||
|
>
|
||||||
|
<naive-provider>
|
||||||
|
<router-view />
|
||||||
|
</naive-provider>
|
||||||
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { subscribeStore } from '@/store';
|
import { NConfigProvider, zhCN, dateZhCN } from 'naive-ui';
|
||||||
import { useTheme } from '@/composables';
|
import { NaiveProvider } from '@/components';
|
||||||
import AppProvider from './AppProvider.vue';
|
import { useThemeStore, subscribeStore } from '@/store';
|
||||||
|
|
||||||
function init() {
|
const theme = useThemeStore();
|
||||||
subscribeStore();
|
|
||||||
useTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
subscribeStore();
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<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 './system';
|
||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './theme';
|
|
||||||
export * from './layout';
|
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 {
|
export enum EnumStorageKey {
|
||||||
|
/** 主题颜色 */
|
||||||
|
'theme-color' = '__THEME_COLOR__',
|
||||||
/** 用户token */
|
/** 用户token */
|
||||||
'token' = '__TOKEN__',
|
'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 './enum';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
export * from './system';
|
export * from './system';
|
||||||
|
export * from './expose';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
|
@ -4,7 +4,11 @@
|
|||||||
<n-breadcrumb-item>
|
<n-breadcrumb-item>
|
||||||
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
||||||
<span>
|
<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>{{ breadcrumb.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
</n-dropdown>
|
</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>
|
<template>
|
||||||
<n-scrollbar>
|
<n-scrollbar class="flex-1-hidden">
|
||||||
<n-menu
|
<n-menu
|
||||||
:value="activeKey"
|
:value="activeKey"
|
||||||
:collapsed="app.siderCollapse"
|
:collapsed="app.siderCollapse"
|
||||||
:collapsed-width="theme.sider.collapsedWidth"
|
:collapsed-width="theme.sider.collapsedWidth"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="menus"
|
:options="routeStore.menus"
|
||||||
:expanded-keys="expandedKeys"
|
:expanded-keys="expandedKeys"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
@update:value="handleUpdateMenu"
|
@update:value="handleUpdateMenu"
|
||||||
@ -21,6 +21,7 @@ import { NScrollbar, NMenu } from 'naive-ui';
|
|||||||
import type { MenuOption } from 'naive-ui';
|
import type { MenuOption } from 'naive-ui';
|
||||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
|
import { getActiveKeyPathsOfMenus } from '@/utils';
|
||||||
import type { GlobalMenuOption } from '@/interface';
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -29,26 +30,9 @@ const theme = useThemeStore();
|
|||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const { routerPush } = useRouterPush();
|
const { routerPush } = useRouterPush();
|
||||||
|
|
||||||
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
|
|
||||||
const activeKey = computed(() => route.name as string);
|
const activeKey = computed(() => route.name as string);
|
||||||
const expandedKeys = ref<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) {
|
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||||
const menuItem = item as GlobalMenuOption;
|
const menuItem = item as GlobalMenuOption;
|
||||||
routerPush(menuItem.routePath);
|
routerPush(menuItem.routePath);
|
||||||
@ -61,7 +45,7 @@ function handleUpdateExpandedKeys(keys: string[]) {
|
|||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
() => {
|
() => {
|
||||||
expandedKeys.value = getExpendedKeys();
|
expandedKeys.value = getActiveKeyPathsOfMenus(activeKey.value, routeStore.menus);
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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>
|
<template>
|
||||||
<dark-mode-container class="global-sider flex-col-stretch h-full">
|
<vertical-sider v-if="!isVerticalMix" class="global-sider" />
|
||||||
<global-logo :show-title="!app.siderCollapse" :style="{ height: theme.header.height + 'px' }" />
|
<vertical-mix-sider v-else class="global-sider" />
|
||||||
<sider-menu class="flex-1-hidden" />
|
|
||||||
</dark-mode-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DarkModeContainer } from '@/components';
|
import { computed } from 'vue';
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
import { VerticalSider, VerticalMixSider } from './components';
|
||||||
import { SiderMenu } from './components';
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
|
|
||||||
|
const isVerticalMix = computed(() => theme.layout.mode === 'vertical-mix');
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.global-sider {
|
.global-sider {
|
||||||
|
13
src/main.ts
13
src/main.ts
@ -1,23 +1,16 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { setupAssets, setupInitSvgLogo } from '@/plugins';
|
import { setupAssets } from '@/plugins';
|
||||||
import { setupRouter } from '@/router';
|
import { setupRouter } from '@/router';
|
||||||
import { setupStore } from '@/store';
|
import { setupStore } from '@/store';
|
||||||
import AppProvider from './AppProvider.vue';
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
async function setupApp() {
|
async function setupApp() {
|
||||||
// 初始化加载的svg logo
|
|
||||||
setupInitSvgLogo('#loadingLogo');
|
|
||||||
|
|
||||||
// 引入静态资源
|
// 引入静态资源
|
||||||
setupAssets();
|
setupAssets();
|
||||||
|
|
||||||
// 挂载 appProvider 解决路由守卫,Axios中可使用,LoadingBar,Dialog,Message 等之类组件
|
|
||||||
const appProvider = createApp(AppProvider);
|
|
||||||
setupStore(appProvider);
|
|
||||||
appProvider.mount('#appProvider');
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
// 挂载pinia状态
|
||||||
setupStore(app);
|
setupStore(app);
|
||||||
|
|
||||||
// 挂载路由
|
// 挂载路由
|
||||||
|
@ -58,6 +58,10 @@ export const useAppStore = defineStore('app-store', {
|
|||||||
/** 设置 vertical-mix模式下 侧边栏的固定状态 */
|
/** 设置 vertical-mix模式下 侧边栏的固定状态 */
|
||||||
setMixSiderIsFixed(isFixed: boolean) {
|
setMixSiderIsFixed(isFixed: boolean) {
|
||||||
this.mixSiderFixed = isFixed;
|
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 { useBodyScroll } from '@/hooks';
|
||||||
import { useAppStore } from '../modules';
|
import { useAppStore } from '../modules';
|
||||||
|
|
||||||
@ -6,8 +7,15 @@ export default function subscribeAppStore() {
|
|||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { scrollBodyHandler } = useBodyScroll();
|
const { scrollBodyHandler } = useBodyScroll();
|
||||||
|
|
||||||
app.$subscribe((_mutation, state) => {
|
// 弹窗打开时禁止滚动条
|
||||||
// 弹窗打开时禁止滚动条
|
const stopHandle = watch(
|
||||||
scrollBodyHandler(state.settingDrawerVisible);
|
() => app.settingDrawerVisible,
|
||||||
|
newValue => {
|
||||||
|
scrollBodyHandler(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopHandle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import subscribeAppStore from './app';
|
import subscribeAppStore from './app';
|
||||||
|
import subscribeThemeStore from './theme';
|
||||||
|
|
||||||
/** 订阅状态 */
|
/** 订阅状态 */
|
||||||
export function subscribeStore() {
|
export function subscribeStore() {
|
||||||
subscribeAppStore();
|
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';
|
import { useThemeStore } from '../modules';
|
||||||
|
|
||||||
/** 订阅app store */
|
/** 订阅theme store */
|
||||||
export default function subscribeAppStore() {
|
export default function subscribeThemeStore() {
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
|
const osTheme = useOsTheme();
|
||||||
|
const { width } = useElementSize(document.documentElement);
|
||||||
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
||||||
|
|
||||||
theme.$subscribe((_mutation, state) => {
|
const stopThemeColor = watch(
|
||||||
// 监听暗黑模式
|
() => theme.themeColor,
|
||||||
if (state.darkMode) {
|
newValue => {
|
||||||
addDarkClass();
|
window.localStorage.setItem(EnumStorageKey['theme-color'], `--primary-color: ${newValue};`);
|
||||||
} else {
|
},
|
||||||
removeDarkClass();
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听暗黑模式
|
||||||
|
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;
|
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,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"~/*": ["./*"]
|
"~/*": ["./*"]
|
||||||
},
|
},
|
||||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmit": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.*", "src/typings/*.d.ts", "src/**/*", "src/**/*.vue", "mock/**/*.ts", "build/**/*.ts", ".env-config.ts"],
|
"include": ["vite.config.*", "src/typings/*.d.ts", "src/**/*", "src/**/*.vue", "mock/**/*.ts", "build/**/*.ts", ".env-config.ts"],
|
||||||
"exclude": ["/dist/**", "node_modules"]
|
"exclude": ["/dist/**", "node_modules"]
|
||||||
|
@ -40,9 +40,7 @@ export default defineConfig({
|
|||||||
'fixed-center': 'fixed left-0 top-0 flex-center wh-full',
|
'fixed-center': 'fixed left-0 top-0 flex-center wh-full',
|
||||||
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
||||||
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
||||||
'transition-base': 'transition-all duration-300 ease-in-out',
|
'transition-base': 'transition-all duration-300 ease-in-out'
|
||||||
// 'dark-transition': "",
|
|
||||||
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
|
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
Reference in New Issue
Block a user