Merge branch 'v2.0' into v2.0-example

This commit is contained in:
Soybean 2025-07-14 00:49:35 +08:00
commit 0fa49f927b
36 changed files with 1315 additions and 594 deletions

View File

@ -127,7 +127,6 @@ function handleClickMask() {
:class="[ :class="[
style['layout-header'], style['layout-header'],
commonClass, commonClass,
headerClass,
headerLeftGapClass, headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab } { 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]" ]"

View File

@ -6,12 +6,6 @@ interface AdminLayoutHeaderConfig {
* @default true * @default true
*/ */
headerVisible?: boolean; headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/** /**
* Header height * Header height
* *

View File

@ -4,7 +4,6 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui'; import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app'; import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme'; import { useThemeStore } from './store/modules/theme';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive'; import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({ defineOptions({
@ -13,7 +12,6 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const authStore = useAuthStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined)); const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
@ -26,13 +24,8 @@ const naiveDateLocale = computed(() => {
}); });
const watermarkProps = computed<WatermarkProps>(() => { const watermarkProps = computed<WatermarkProps>(() => {
const content =
themeStore.watermark.enableUserName && authStore.userInfo.userName
? authStore.userInfo.userName
: themeStore.watermark.text;
return { return {
content, content: themeStore.watermarkContent,
cross: true, cross: true,
fullscreen: true, fullscreen: true,
fontSize: 16, fontSize: 16,

View File

@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__'; export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = { export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.themeSchema.light', light: 'theme.appearance.themeSchema.light',
dark: 'theme.themeSchema.dark', dark: 'theme.appearance.themeSchema.dark',
auto: 'theme.themeSchema.auto' auto: 'theme.appearance.themeSchema.auto'
}; };
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord); export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
@ -21,45 +21,57 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
}; };
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = { export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
vertical: 'theme.layoutMode.vertical', vertical: 'theme.layout.layoutMode.vertical',
'vertical-mix': 'theme.layoutMode.vertical-mix', 'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
horizontal: 'theme.layoutMode.horizontal', 'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
'horizontal-mix': 'theme.layoutMode.horizontal-mix' horizontal: 'theme.layout.layoutMode.horizontal',
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
}; };
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord); export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = { export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
wrapper: 'theme.scrollMode.wrapper', wrapper: 'theme.layout.content.scrollMode.wrapper',
content: 'theme.scrollMode.content' content: 'theme.layout.content.scrollMode.content'
}; };
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord); export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = { export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.tab.mode.chrome', chrome: 'theme.layout.tab.mode.chrome',
button: 'theme.tab.mode.button' button: 'theme.layout.tab.mode.button'
}; };
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord); export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = { export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
'fade-slide': 'theme.page.mode.fade-slide', 'fade-slide': 'theme.layout.content.page.mode.fade-slide',
fade: 'theme.page.mode.fade', fade: 'theme.layout.content.page.mode.fade',
'fade-bottom': 'theme.page.mode.fade-bottom', 'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
'fade-scale': 'theme.page.mode.fade-scale', 'fade-scale': 'theme.layout.content.page.mode.fade-scale',
'zoom-fade': 'theme.page.mode.zoom-fade', 'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
'zoom-out': 'theme.page.mode.zoom-out', 'zoom-out': 'theme.layout.content.page.mode.zoom-out',
none: 'theme.page.mode.none' none: 'theme.layout.content.page.mode.none'
}; };
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord); export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = { export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.resetCacheStrategy.close', close: 'theme.layout.resetCacheStrategy.close',
refresh: 'theme.resetCacheStrategy.refresh' refresh: 'theme.layout.resetCacheStrategy.refresh'
}; };
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord); export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
export const DARK_CLASS = 'dark'; export const DARK_CLASS = 'dark';
export const watermarkTimeFormatOptions = [
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
{ label: 'HH:mm', value: 'HH:mm' },
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
];

View File

@ -29,7 +29,7 @@ const layoutMode = computed(() => {
}); });
const headerProps = computed(() => { const headerProps = computed(() => {
const { mode, reverseHorizontalMix } = themeStore.layout; const { mode } = themeStore.layout;
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = { const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: { vertical: {
@ -42,15 +42,25 @@ const headerProps = computed(() => {
showMenu: false, showMenu: false,
showMenuToggler: false showMenuToggler: false
}, },
'vertical-hybrid-header-first': {
showLogo: !isActiveFirstLevelMenuHasChildren.value,
showMenu: true,
showMenuToggler: false
},
horizontal: { horizontal: {
showLogo: true, showLogo: true,
showMenu: true, showMenu: true,
showMenuToggler: false showMenuToggler: false
}, },
'horizontal-mix': { 'top-hybrid-sidebar-first': {
showLogo: true, showLogo: true,
showMenu: true, showMenu: true,
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value showMenuToggler: false
},
'top-hybrid-header-first': {
showLogo: true,
showMenu: true,
showMenuToggler: isActiveFirstLevelMenuHasChildren.value
} }
}; };
@ -61,44 +71,56 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix'); const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix'); const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const siderWidth = computed(() => getSiderWidth()); const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth()); const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderWidth() { function getSiderAndCollapsedWidth(isCollapsed: boolean) {
const { reverseHorizontalMix } = themeStore.layout; const {
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider; mixChildMenuWidth,
collapsedWidth,
width: themeWidth,
mixCollapsedWidth,
mixWidth: themeMixWidth
} = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) { const width = isCollapsed ? collapsedWidth : themeWidth;
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
if (isTopHybridHeaderFirst.value) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0; return isActiveFirstLevelMenuHasChildren.value ? width : 0;
} }
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width; if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
return 0;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
} }
return w; const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
let finalWidth = isMixMode ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
}
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
}
return finalWidth;
}
function getSiderWidth() {
return getSiderAndCollapsedWidth(false);
} }
function getSiderCollapsedWidth() { function getSiderCollapsedWidth() {
const { reverseHorizontalMix } = themeStore.layout; return getSiderAndCollapsedWidth(true);
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
return w;
} }
</script> </script>

View File

@ -1,7 +1,9 @@
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks'; import { useContext } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu); export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
@ -9,6 +11,17 @@ function useMixMenu() {
const route = useRoute(); const route = useRoute();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const activeFirstLevelMenuKey = ref(''); const activeFirstLevelMenuKey = ref('');
@ -22,20 +35,6 @@ function useMixMenu() {
setActiveFirstLevelMenuKey(firstLevelRouteName); setActiveFirstLevelMenuKey(firstLevelRouteName);
} }
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const childLevelMenus = computed<App.Global.Menu[]>(
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const isActiveFirstLevelMenuHasChildren = computed(() => { const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) { if (!activeFirstLevelMenuKey.value) {
return false; return false;
@ -46,6 +45,61 @@ function useMixMenu() {
return Boolean(findItem?.children?.length); return Boolean(findItem?.children?.length);
}); });
function handleSelectFirstLevelMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const secondLevelMenus = computed<App.Global.Menu[]>(
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const activeSecondLevelMenuKey = ref('');
function setActiveSecondLevelMenuKey(key: string) {
activeSecondLevelMenuKey.value = key;
}
function getActiveSecondLevelMenuKey() {
const keys = selectedKey.value.split('_');
if (keys.length < 2) {
setActiveSecondLevelMenuKey('');
return;
}
const [firstLevelRouteName, level2SuffixName] = keys;
const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
setActiveSecondLevelMenuKey(secondLevelRouteName);
}
const isActiveSecondLevelMenuHasChildren = computed(() => {
if (!activeSecondLevelMenuKey.value) {
return false;
}
const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
function handleSelectSecondLevelMenu(key: RouteKey) {
setActiveSecondLevelMenuKey(key);
if (!isActiveSecondLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const childLevelMenus = computed<App.Global.Menu[]>(
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
);
watch( watch(
() => route.name, () => route.name,
() => { () => {
@ -55,13 +109,19 @@ function useMixMenu() {
); );
return { return {
allMenus,
firstLevelMenus, firstLevelMenus,
childLevelMenus,
isActiveFirstLevelMenuHasChildren,
activeFirstLevelMenuKey, activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey, setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey isActiveFirstLevelMenuHasChildren,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
setActiveSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
}; };
} }

View File

@ -3,6 +3,7 @@ import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core'; import { createReusableTemplate } from '@vueuse/core';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color'; import { transformColorWithOpacity } from '@sa/color';
import type { RouteKey } from '@elegant-router/types';
defineOptions({ defineOptions({
name: 'FirstLevelMenu' name: 'FirstLevelMenu'
@ -20,7 +21,7 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
interface Emits { interface Emits {
(e: 'select', menu: App.Global.Menu): boolean; (e: 'select', menuKey: RouteKey): boolean;
(e: 'toggleSiderCollapse'): void; (e: 'toggleSiderCollapse'): void;
} }
@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
return darkMode ? dark : light; return darkMode ? dark : light;
}); });
function handleClickMixMenu(menu: App.Global.Menu) { function handleClickMixMenu(menuKey: RouteKey) {
emit('select', menu); emit('select', menuKey);
} }
function toggleSiderCollapse() { function toggleSiderCollapse() {
@ -88,7 +89,7 @@ function toggleSiderCollapse() {
:icon="menu.icon" :icon="menu.icon"
:active="menu.key === activeMenuKey" :active="menu.key === activeMenuKey"
:is-mini="siderCollapse" :is-mini="siderCollapse"
@click="handleClickMixMenu(menu)" @click="handleClickMixMenu(menu.routeKey)"
/> />
</SimpleScrollbar> </SimpleScrollbar>
<MenuToggler <MenuToggler

View File

@ -5,9 +5,10 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import VerticalMenu from './modules/vertical-menu.vue'; import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue'; import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
import HorizontalMenu from './modules/horizontal-menu.vue'; import HorizontalMenu from './modules/horizontal-menu.vue';
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue'; import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue'; import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
defineOptions({ defineOptions({
name: 'GlobalMenu' name: 'GlobalMenu'
@ -20,8 +21,10 @@ const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = { const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu, vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu, 'vertical-mix': VerticalMixMenu,
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
horizontal: HorizontalMenu, horizontal: HorizontalMenu,
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu 'top-hybrid-sidebar-first': TopHybridSidebarFirst,
'top-hybrid-header-first': TopHybridHeaderFirst
}; };
return menuMap[themeStore.layout.mode]; return menuMap[themeStore.layout.mode];

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
@ -11,7 +10,7 @@ import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'ReversedHorizontalMixMenu' name: 'TopHybridHeaderFirst'
}); });
const route = useRoute(); const route = useRoute();
@ -19,23 +18,9 @@ const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
firstLevelMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren
} = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() { function updateExpandedKeys() {
@ -63,7 +48,7 @@ watch(
:options="firstLevelMenus" :options="firstLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="handleSelectMixMenu" @update:value="handleSelectFirstLevelMenu"
/> />
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
@ -75,7 +60,7 @@ watch(
:collapsed="appStore.siderCollapse" :collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth" :collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="childLevelMenus" :options="secondLevelMenus"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
/> />

View File

@ -7,22 +7,14 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMenu, useMixMenuContext } from '../../../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'HorizontalMixMenu' name: 'TopHybridSidebarFirst'
}); });
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext(); const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
</script> </script>
<template> <template>
@ -30,7 +22,7 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
<NMenu <NMenu
mode="horizontal" mode="horizontal"
:value="selectedKey" :value="selectedKey"
:options="childLevelMenus" :options="secondLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
@ -38,12 +30,12 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<FirstLevelMenu <FirstLevelMenu
:menus="allMenus" :menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey" :active-menu-key="activeFirstLevelMenuKey"
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectMixMenu" @select="handleSelectFirstLevelMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
/> />
</Teleport> </Teleport>

View File

@ -0,0 +1,149 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
defineOptions({
name: 'VerticalHybridHeaderFirst'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const {
firstLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
} = useMixMenuContext();
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key);
if (isActiveSecondLevelMenuHasChildren.value) {
setDrawerVisible(true);
}
}
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
if (secondLevelMenus.value.length > 0) {
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
}
}
function handleResetActiveMenu() {
setDrawerVisible(false);
if (!appStore.mixSiderFixed) {
getActiveFirstLevelMenuKey();
getActiveSecondLevelMenuKey();
}
}
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="activeFirstLevelMenuKey"
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu
:menus="secondLevelMenus"
:active-menu-key="activeSecondLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="inverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:options="childLevelMenus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</DarkModeContainer>
</div>
</div>
</Teleport>
</template>
<style scoped></style>

View File

@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
@ -24,28 +25,26 @@ const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean(); const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const { const {
allMenus, firstLevelMenus,
childLevelMenus, secondLevelMenus,
activeFirstLevelMenuKey, activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey, isActiveFirstLevelMenuHasChildren,
getActiveFirstLevelMenuKey getActiveFirstLevelMenuKey,
// handleSelectFirstLevelMenu
} = useMixMenuContext(); } = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted); const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0); const hasChildMenus = computed(() => secondLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed)); const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) { function handleSelectMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(menu.key); handleSelectFirstLevelMenu(key);
if (menu.children?.length) { if (isActiveFirstLevelMenuHasChildren.value) {
setDrawerVisible(true); setDrawerVisible(true);
} else {
routerPushByKeyWithMetaQuery(menu.routeKey);
} }
} }
@ -80,13 +79,13 @@ watch(
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu"> <div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu <FirstLevelMenu
:menus="allMenus" :menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey" :active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted" :inverted="inverted"
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectMixMenu" @select="handleSelectMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
> >
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" /> <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
@ -113,7 +112,7 @@ watch(
v-model:expanded-keys="expandedKeys" v-model:expanded-keys="expandedKeys"
mode="vertical" mode="vertical"
:value="selectedKey" :value="selectedKey"
:options="childLevelMenus" :options="secondLevelMenus"
:inverted="inverted" :inverted="inverted"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"

View File

@ -12,10 +12,13 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix'); const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix'); const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted); const darkMenu = computed(
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value); () =>
!themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
);
const showLogo = computed(() => themeStore.layout.mode === 'vertical');
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full')); const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
</script> </script>

View File

@ -27,7 +27,6 @@ type LayoutConfig = Record<
UnionKey.ThemeLayoutMode, UnionKey.ThemeLayoutMode,
{ {
placement: PopoverPlacement; placement: PopoverPlacement;
headerClass: string;
menuClass: string; menuClass: string;
mainClass: string; mainClass: string;
} }
@ -36,25 +35,31 @@ type LayoutConfig = Record<
const layoutConfig: LayoutConfig = { const layoutConfig: LayoutConfig = {
vertical: { vertical: {
placement: 'bottom', placement: 'bottom',
headerClass: '',
menuClass: 'w-1/3 h-full', menuClass: 'w-1/3 h-full',
mainClass: 'w-2/3 h-3/4' mainClass: 'w-2/3 h-3/4'
}, },
'vertical-mix': { 'vertical-mix': {
placement: 'bottom', placement: 'bottom',
headerClass: '', menuClass: 'w-1/4 h-full',
mainClass: 'w-2/3 h-3/4'
},
'vertical-hybrid-header-first': {
placement: 'bottom',
menuClass: 'w-1/4 h-full', menuClass: 'w-1/4 h-full',
mainClass: 'w-2/3 h-3/4' mainClass: 'w-2/3 h-3/4'
}, },
horizontal: { horizontal: {
placement: 'bottom', placement: 'bottom',
headerClass: '',
menuClass: 'w-full h-1/4', menuClass: 'w-full h-1/4',
mainClass: 'w-full h-3/4' mainClass: 'w-full h-3/4'
}, },
'horizontal-mix': { 'top-hybrid-sidebar-first': {
placement: 'bottom',
menuClass: 'w-full h-1/4',
mainClass: 'w-2/3 h-3/4'
},
'top-hybrid-header-first': {
placement: 'bottom', placement: 'bottom',
headerClass: '',
menuClass: 'w-full h-1/4', menuClass: 'w-full h-1/4',
mainClass: 'w-2/3 h-3/4' mainClass: 'w-2/3 h-3/4'
} }
@ -68,25 +73,27 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
</script> </script>
<template> <template>
<div class="flex-center flex-wrap gap-x-32px gap-y-16px"> <div class="grid grid-cols-2 gap-x-16px gap-y-12px md:grid-cols-3">
<div <div
v-for="(item, key) in layoutConfig" v-for="(item, key) in layoutConfig"
:key="key" :key="key"
class="flex cursor-pointer border-2px rounded-6px hover:border-primary" class="flex-col-center cursor-pointer"
:class="[mode === key ? 'border-primary' : 'border-transparent']"
@click="handleChangeMode(key)" @click="handleChangeMode(key)"
> >
<NTooltip :placement="item.placement"> <NTooltip :placement="item.placement">
<template #trigger> <template #trigger>
<div <div
class="h-64px w-96px gap-6px rd-4px p-6px shadow dark:shadow-coolGray-5" class="h-64px w-96px gap-6px rd-4px p-6px shadow ring-2 ring-transparent transition-all hover:ring-primary"
:class="[key.includes('vertical') ? 'flex' : 'flex-col']" :class="{ '!ring-primary': mode === key }"
> >
<div class="h-full w-full gap-1" :class="[key.includes('vertical') ? 'flex' : 'flex-col']">
<slot :name="key"></slot> <slot :name="key"></slot>
</div> </div>
</div>
</template> </template>
{{ $t(themeLayoutModeRecord[key]) }} {{ $t(`theme.layout.layoutMode.${key}_detail`) }}
</NTooltip> </NTooltip>
<p class="mt-8px text-12px">{{ $t(themeLayoutModeRecord[key]) }}</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales'; import { $t } from '@/locales';
import DarkMode from './modules/dark-mode.vue'; import AppearanceSettings from './modules/appearance/index.vue';
import LayoutMode from './modules/layout-mode.vue'; import LayoutSettings from './modules/layout/index.vue';
import ThemeColor from './modules/theme-color.vue'; import GeneralSettings from './modules/general/index.vue';
import PageFun from './modules/page-fun.vue';
import ConfigOperation from './modules/config-operation.vue'; import ConfigOperation from './modules/config-operation.vue';
defineOptions({ defineOptions({
@ -12,15 +12,37 @@ defineOptions({
}); });
const appStore = useAppStore(); const appStore = useAppStore();
const activeTab = ref('appearance');
const drawerWidth = computed(() => {
const width = 400;
// On mobile devices, use 90% of viewport width with a maximum of 400px
if (appStore.isMobile) {
return `min(90vw, ${width}px)`;
}
return width;
});
</script> </script>
<template> <template>
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="360"> <NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="drawerWidth">
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable> <NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
<DarkMode /> <NTabs v-model:value="activeTab" type="segment" size="medium" class="mb-16px">
<LayoutMode /> <NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
<ThemeColor /> <NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
<PageFun /> <NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
</NTabs>
<div class="min-h-400px">
<KeepAlive>
<AppearanceSettings v-if="activeTab === 'appearance'" />
<LayoutSettings v-else-if="activeTab === 'layout'" />
<GeneralSettings v-else-if="activeTab === 'general'" />
</KeepAlive>
</div>
<template #footer> <template #footer>
<ConfigOperation /> <ConfigOperation />
</template> </template>
@ -28,4 +50,14 @@ const appStore = useAppStore();
</NDrawer> </NDrawer>
</template> </template>
<style scoped></style> <style scoped>
:deep(.n-tab) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.n-tab-pane) {
padding: 0;
}
</style>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import ThemeSchema from './modules/theme-schema.vue';
import ThemeColor from './modules/theme-color.vue';
defineOptions({
name: 'AppearanceSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<ThemeSchema />
<ThemeColor />
</div>
</template>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales'; import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue'; import SettingItem from '../../../components/setting-item.vue';
defineOptions({ defineOptions({
name: 'ThemeColor' name: 'ThemeColor'
@ -34,16 +34,16 @@ const swatches: string[] = [
</script> </script>
<template> <template>
<NDivider>{{ $t('theme.themeColor.title') }}</NDivider> <NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
<div class="flex-col-stretch gap-12px"> <div class="flex-col-stretch gap-12px">
<NTooltip placement="top-start"> <NTooltip placement="top-start">
<template #trigger> <template #trigger>
<SettingItem key="recommend-color" :label="$t('theme.recommendColor')"> <SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
<NSwitch v-model:value="themeStore.recommendColor" /> <NSwitch v-model:value="themeStore.recommendColor" />
</SettingItem> </SettingItem>
</template> </template>
<p> <p>
<span class="pr-12px">{{ $t('theme.recommendColorDesc') }}</span> <span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
<br /> <br />
<NButton <NButton
text text
@ -57,10 +57,14 @@ const swatches: string[] = [
</NButton> </NButton>
</p> </p>
</NTooltip> </NTooltip>
<SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)"> <SettingItem
v-for="(_, key) in themeStore.themeColors"
:key="key"
:label="$t(`theme.appearance.themeColor.${key}`)"
>
<template v-if="key === 'info'" #suffix> <template v-if="key === 'info'" #suffix>
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary"> <NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
{{ $t('theme.themeColor.followPrimary') }} {{ $t('theme.appearance.themeColor.followPrimary') }}
</NCheckbox> </NCheckbox>
</template> </template>
<NColorPicker <NColorPicker

View File

@ -3,10 +3,10 @@ import { computed } from 'vue';
import { themeSchemaRecord } from '@/constants/app'; import { themeSchemaRecord } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales'; import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue'; import SettingItem from '../../../components/setting-item.vue';
defineOptions({ defineOptions({
name: 'DarkMode' name: 'ThemeSchema'
}); });
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -33,7 +33,7 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
</script> </script>
<template> <template>
<NDivider>{{ $t('theme.themeSchema.title') }}</NDivider> <NDivider>{{ $t('theme.appearance.themeSchema.title') }}</NDivider>
<div class="flex-col-stretch gap-16px"> <div class="flex-col-stretch gap-16px">
<div class="i-flex-center"> <div class="i-flex-center">
<NTabs <NTabs
@ -50,14 +50,14 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
</NTabs> </NTabs>
</div> </div>
<Transition name="sider-inverted"> <Transition name="sider-inverted">
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')"> <SettingItem v-if="showSiderInverted" :label="$t('theme.layout.sider.inverted')">
<NSwitch v-model:value="themeStore.sider.inverted" /> <NSwitch v-model:value="themeStore.sider.inverted" />
</SettingItem> </SettingItem>
</Transition> </Transition>
<SettingItem :label="$t('theme.grayscale')"> <SettingItem :label="$t('theme.appearance.grayscale')">
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" /> <NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
</SettingItem> </SettingItem>
<SettingItem :label="$t('theme.colourWeakness')"> <SettingItem :label="$t('theme.appearance.colourWeakness')">
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" /> <NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
</SettingItem> </SettingItem>
</div> </div>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import GlobalSettings from './modules/global-settings.vue';
import WatermarkSettings from './modules/watermark-settings.vue';
defineOptions({
name: 'GeneralSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<GlobalSettings />
<WatermarkSettings />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'GlobalSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.general.title') }}</NDivider>
<SettingItem :label="$t('theme.general.multilingual.visible')">
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
</SettingItem>
<SettingItem :label="$t('theme.general.globalSearch.visible')">
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
</SettingItem>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { computed } from 'vue';
import { watermarkTimeFormatOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'WatermarkSettings'
});
const themeStore = useThemeStore();
const isWatermarkTextVisible = computed(
() => themeStore.watermark.visible && !themeStore.watermark.enableUserName && !themeStore.watermark.enableTime
);
</script>
<template>
<NDivider>{{ $t('theme.general.watermark.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.general.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
<NSwitch :value="themeStore.watermark.enableUserName" @update:value="themeStore.setWatermarkEnableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.enableTime')">
<NSwitch :value="themeStore.watermark.enableTime" @update:value="themeStore.setWatermarkEnableTime" />
</SettingItem>
<SettingItem
v-if="themeStore.watermark.visible && themeStore.watermark.enableTime"
key="4"
:label="$t('theme.general.watermark.timeFormat')"
>
<NSelect
v-model:value="themeStore.watermark.timeFormat"
:options="watermarkTimeFormatOptions"
size="small"
class="w-210px"
/>
</SettingItem>
<SettingItem v-if="isWatermarkTextVisible" key="5" :label="$t('theme.general.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import LayoutMode from './modules/layout-mode.vue';
import TabSettings from './modules/tab-settings.vue';
import HeaderSettings from './modules/header-settings.vue';
import SiderSettings from './modules/sider-settings.vue';
import FooterSettings from './modules/footer-settings.vue';
import ContentSettings from './modules/content-settings.vue';
defineOptions({
name: 'LayoutSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<div class="flex-col-stretch gap-16px">
<LayoutMode />
<TabSettings />
<HeaderSettings />
<!-- The top menu mode does not have a sidebar -->
<SiderSettings v-if="themeStore.layout.mode !== 'horizontal'" />
<FooterSettings />
<ContentSettings />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed } from 'vue';
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'ContentSettings'
});
const themeStore = useThemeStore();
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
</script>
<template>
<NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
<NSelect
v-model:value="themeStore.layout.scrollMode"
:options="translateOptions(themeScrollModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="2" :label="$t('theme.layout.content.page.animate')">
<NSwitch v-model:value="themeStore.page.animate" />
</SettingItem>
<SettingItem v-if="themeStore.page.animate" key="3" :label="$t('theme.layout.content.page.mode.title')">
<NSelect
v-model:value="themeStore.page.animateMode"
:options="translateOptions(themePageAnimationModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="isWrapperScrollMode" key="4" :label="$t('theme.layout.content.fixedHeaderAndTab')">
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'FooterSettings'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
const isMixHorizontalMode = computed(() =>
['top-hybrid-sidebar-first', 'top-hybrid-header-first'].includes(layoutMode.value)
);
</script>
<template>
<NDivider>{{ $t('theme.layout.footer.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.footer.visible')">
<NSwitch v-model:value="themeStore.footer.visible" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && isWrapperScrollMode"
key="2"
:label="$t('theme.layout.footer.fixed')"
>
<NSwitch v-model:value="themeStore.footer.fixed" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible" key="3" :label="$t('theme.layout.footer.height')">
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && isMixHorizontalMode"
key="4"
:label="$t('theme.layout.footer.right')"
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'HeaderSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.layout.header.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.header.height')">
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="2" :label="$t('theme.layout.header.breadcrumb.visible')">
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
</SettingItem>
<SettingItem
v-if="themeStore.header.breadcrumb.visible"
key="3"
:label="$t('theme.layout.header.breadcrumb.showIcon')"
>
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -2,8 +2,7 @@
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales'; import { $t } from '@/locales';
import LayoutModeCard from '../components/layout-mode-card.vue'; import LayoutModeCard from '../../../components/layout-mode-card.vue';
import SettingItem from '../components/setting-item.vue';
defineOptions({ defineOptions({
name: 'LayoutMode' name: 'LayoutMode'
@ -11,56 +10,60 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
function handleReverseHorizontalMixChange(value: boolean) {
themeStore.setLayoutReverseHorizontalMix(value);
}
</script> </script>
<template> <template>
<NDivider>{{ $t('theme.layoutMode.title') }}</NDivider> <NDivider>{{ $t('theme.layout.layoutMode.title') }}</NDivider>
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile"> <LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
<template #vertical> <template #vertical>
<div class="layout-sider h-full w-18px"></div> <div class="layout-sider h-full w-18px !bg-primary"></div>
<div class="vertical-wrapper"> <div class="vertical-wrapper">
<div class="layout-header"></div> <div class="layout-header bg-primary-200"></div>
<div class="layout-main"></div> <div class="layout-main"></div>
</div> </div>
</template> </template>
<template #vertical-mix> <template #vertical-mix>
<div class="layout-sider h-full w-8px"></div> <div class="layout-sider h-full w-8px !bg-primary"></div>
<div class="layout-sider h-full w-16px"></div> <div class="layout-sider h-full w-16px !bg-primary-300"></div>
<div class="vertical-wrapper"> <div class="vertical-wrapper">
<div class="layout-header"></div> <div class="layout-header bg-primary-200"></div>
<div class="layout-main"></div>
</div>
</template>
<template #vertical-hybrid-header-first>
<div class="layout-sider h-full w-8px !bg-primary"></div>
<div class="layout-sider h-full w-16px !bg-primary-300"></div>
<div class="vertical-wrapper">
<div class="layout-header bg-primary"></div>
<div class="layout-main"></div> <div class="layout-main"></div>
</div> </div>
</template> </template>
<template #horizontal> <template #horizontal>
<div class="layout-header"></div> <div class="layout-header !bg-primary"></div>
<div class="horizontal-wrapper"> <div class="horizontal-wrapper">
<div class="layout-main"></div> <div class="layout-main"></div>
</div> </div>
</template> </template>
<template #horizontal-mix> <template #top-hybrid-sidebar-first>
<div class="layout-header"></div> <div class="layout-header !bg-primary-300"></div>
<div class="horizontal-wrapper">
<div class="layout-sider w-18px !bg-primary"></div>
<div class="layout-main"></div>
</div>
</template>
<template #top-hybrid-header-first>
<div class="layout-header bg-primary"></div>
<div class="horizontal-wrapper"> <div class="horizontal-wrapper">
<div class="layout-sider w-18px"></div> <div class="layout-sider w-18px"></div>
<div class="layout-main"></div> <div class="layout-main"></div>
</div> </div>
</template> </template>
</LayoutModeCard> </LayoutModeCard>
<SettingItem
v-if="themeStore.layout.mode === 'horizontal-mix'"
:label="$t('theme.layoutMode.reverseHorizontalMix')"
class="mt-16px"
>
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
</SettingItem>
</template> </template>
<style scoped> <style scoped>
.layout-header { .layout-header {
--uno: h-16px bg-primary rd-4px; --uno: h-16px rd-4px;
} }
.layout-sider { .layout-sider {

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'SiderSettings'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
</script>
<template>
<NDivider>{{ $t('theme.layout.sider.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem v-if="layoutMode === 'vertical'" key="1" :label="$t('theme.layout.sider.width')">
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="2" :label="$t('theme.layout.sider.collapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="3" :label="$t('theme.layout.sider.mixWidth')">
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="4" :label="$t('theme.layout.sider.mixCollapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical-mix'" key="5" :label="$t('theme.layout.sider.mixChildMenuWidth')">
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import { resetCacheStrategyOptions, themeTabModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'TabSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.layout.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.layout.tab.visible')">
<NSwitch v-model:value="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
<NSwitch v-model:value="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="4" :label="$t('theme.layout.tab.mode.title')">
<NSelect
v-model:value="themeStore.tab.mode"
:options="translateOptions(themeTabModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -1,157 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
resetCacheStrategyOptions,
themePageAnimationModeOptions,
themeScrollModeOptions,
themeTabModeOptions
} from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue';
defineOptions({
name: 'PageFun'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
</script>
<template>
<NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.scrollMode.title')">
<NSelect
v-model:value="themeStore.layout.scrollMode"
:options="translateOptions(themeScrollModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1-1" :label="$t('theme.page.animate')">
<NSwitch v-model:value="themeStore.page.animate" />
</SettingItem>
<SettingItem v-if="themeStore.page.animate" key="1-2" :label="$t('theme.page.mode.title')">
<NSelect
v-model:value="themeStore.page.animateMode"
:options="translateOptions(themePageAnimationModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="isWrapperScrollMode" key="2" :label="$t('theme.fixedHeaderAndTab')">
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
</SettingItem>
<SettingItem key="3" :label="$t('theme.header.height')">
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="4" :label="$t('theme.header.breadcrumb.visible')">
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
</SettingItem>
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
</SettingItem>
<SettingItem key="5" :label="$t('theme.tab.visible')">
<NSwitch v-model:value="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
<NSwitch v-model:value="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
<NSelect
v-model:value="themeStore.tab.mode"
:options="translateOptions(themeTabModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="6-2" :label="$t('theme.sider.collapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="6-3" :label="$t('theme.sider.mixWidth')">
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="6-4" :label="$t('theme.sider.mixCollapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical-mix'" key="6-5" :label="$t('theme.sider.mixChildMenuWidth')">
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="7" :label="$t('theme.footer.visible')">
<NSwitch v-model:value="themeStore.footer.visible" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible && isWrapperScrollMode" key="7-1" :label="$t('theme.footer.fixed')">
<NSwitch v-model:value="themeStore.footer.fixed" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible" key="7-2" :label="$t('theme.footer.height')">
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && layoutMode === 'horizontal-mix'"
key="7-3"
:label="$t('theme.footer.right')"
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
<SettingItem key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-2" :label="$t('theme.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
<SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
</SettingItem>
<SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -58,6 +58,13 @@ const local: App.I18n.Schema = {
tokenExpired: 'The requested token has expired' tokenExpired: 'The requested token has expired'
}, },
theme: { theme: {
themeDrawerTitle: 'Theme Configuration',
tabs: {
appearance: 'Appearance',
layout: 'Layout',
general: 'General'
},
appearance: {
themeSchema: { themeSchema: {
title: 'Theme Schema', title: 'Theme Schema',
light: 'Light', light: 'Light',
@ -66,16 +73,6 @@ const local: App.I18n.Schema = {
}, },
grayscale: 'Grayscale', grayscale: 'Grayscale',
colourWeakness: 'Colour Weakness', colourWeakness: 'Colour Weakness',
layoutMode: {
title: 'Layout Mode',
vertical: 'Vertical Menu Mode',
horizontal: 'Horizontal Menu Mode',
'vertical-mix': 'Vertical Mix Menu Mode',
'horizontal-mix': 'Horizontal Mix menu Mode',
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to',
themeColor: { themeColor: {
title: 'Theme Color', title: 'Theme Color',
primary: 'Primary', primary: 'Primary',
@ -85,6 +82,66 @@ const local: App.I18n.Schema = {
error: 'Error', error: 'Error',
followPrimary: 'Follow Primary' followPrimary: 'Follow Primary'
}, },
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to'
},
layout: {
layoutMode: {
title: 'Layout Mode',
vertical: 'Vertical Mode',
horizontal: 'Horizontal Mode',
'vertical-mix': 'Vertical Mix Mode',
'vertical-hybrid-header-first': 'Left Hybrid Header-First',
'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
'top-hybrid-header-first': 'Top-Hybrid Header-First',
vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
'vertical-mix_detail':
'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter left side.',
'vertical-hybrid-header-first_detail':
'Left hybrid layout, with the primary menu at the top, the secondary menu on the dark left side, and the tertiary menu on the lighter left side.',
horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
'top-hybrid-sidebar-first_detail':
'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
'top-hybrid-header-first_detail':
'Top hybrid layout, with the primary menu at the top and the secondary menu on the left.'
},
tab: {
title: 'Tab Settings',
visible: 'Tab Visible',
cache: 'Tag Bar Info Cache',
height: 'Tab Height',
mode: {
title: 'Tab Mode',
chrome: 'Chrome',
button: 'Button'
}
},
header: {
title: 'Header Settings',
height: 'Header Height',
breadcrumb: {
visible: 'Breadcrumb Visible',
showIcon: 'Breadcrumb Icon Visible'
}
},
sider: {
title: 'Sider Settings',
inverted: 'Dark Sider',
width: 'Sider Width',
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
},
footer: {
title: 'Footer Settings',
visible: 'Footer Visible',
fixed: 'Fixed Footer',
height: 'Footer Height',
right: 'Right Footer'
},
content: {
title: 'Content Area Settings',
scrollMode: { scrollMode: {
title: 'Scroll Mode', title: 'Scroll Mode',
wrapper: 'Wrapper', wrapper: 'Wrapper',
@ -103,55 +160,32 @@ const local: App.I18n.Schema = {
none: 'None' none: 'None'
} }
}, },
fixedHeaderAndTab: 'Fixed Header And Tab', fixedHeaderAndTab: 'Fixed Header And Tab'
header: {
height: 'Header Height',
breadcrumb: {
visible: 'Breadcrumb Visible',
showIcon: 'Breadcrumb Icon Visible'
}, },
multilingual: {
visible: 'Display multilingual button'
},
globalSearch: {
visible: 'Display GlobalSearch button'
}
},
tab: {
visible: 'Tab Visible',
cache: 'Tag Bar Info Cache',
height: 'Tab Height',
mode: {
title: 'Tab Mode',
chrome: 'Chrome',
button: 'Button'
}
},
sider: {
inverted: 'Dark Sider',
width: 'Sider Width',
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
},
footer: {
visible: 'Footer Visible',
fixed: 'Fixed Footer',
height: 'Footer Height',
right: 'Right Footer'
},
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text',
enableUserName: 'Enable User Name Watermark'
},
themeDrawerTitle: 'Theme Configuration',
pageFunTitle: 'Page Function',
resetCacheStrategy: { resetCacheStrategy: {
title: 'Reset Cache Strategy', title: 'Reset Cache Strategy',
close: 'Close Page', close: 'Close Page',
refresh: 'Refresh Page' refresh: 'Refresh Page'
}
},
general: {
title: 'General Settings',
watermark: {
title: 'Watermark Settings',
visible: 'Watermark Full Screen Visible',
text: 'Custom Watermark Text',
enableUserName: 'Enable User Name Watermark',
enableTime: 'Show Current Time',
timeFormat: 'Time Format'
},
multilingual: {
title: 'Multilingual Settings',
visible: 'Display multilingual button'
},
globalSearch: {
title: 'Global Search Settings',
visible: 'Display GlobalSearch button'
}
}, },
configOperation: { configOperation: {
copyConfig: 'Copy Config', copyConfig: 'Copy Config',

View File

@ -58,6 +58,13 @@ const local: App.I18n.Schema = {
tokenExpired: 'token已过期' tokenExpired: 'token已过期'
}, },
theme: { theme: {
themeDrawerTitle: '主题配置',
tabs: {
appearance: '外观',
layout: '布局',
general: '通用'
},
appearance: {
themeSchema: { themeSchema: {
title: '主题模式', title: '主题模式',
light: '亮色模式', light: '亮色模式',
@ -66,16 +73,6 @@ const local: App.I18n.Schema = {
}, },
grayscale: '灰色模式', grayscale: '灰色模式',
colourWeakness: '色弱模式', colourWeakness: '色弱模式',
layoutMode: {
title: '布局模式',
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
horizontal: '顶部菜单模式',
'horizontal-mix': '顶部菜单混合模式',
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照',
themeColor: { themeColor: {
title: '主题颜色', title: '主题颜色',
primary: '主色', primary: '主色',
@ -85,6 +82,63 @@ const local: App.I18n.Schema = {
error: '错误色', error: '错误色',
followPrimary: '跟随主色' followPrimary: '跟随主色'
}, },
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照'
},
layout: {
layoutMode: {
title: '布局模式',
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
'vertical-hybrid-header-first': '左侧混合-顶部优先',
horizontal: '顶部菜单模式',
'top-hybrid-sidebar-first': '顶部混合-侧边优先',
'top-hybrid-header-first': '顶部混合-顶部优先',
vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
'vertical-hybrid-header-first_detail':
'左侧混合布局,一级菜单在顶部,二级菜单在左侧深色区域,三级菜单在左侧浅色区域。',
horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
},
tab: {
title: '标签栏设置',
visible: '显示标签栏',
cache: '标签栏信息缓存',
height: '标签栏高度',
mode: {
title: '标签栏风格',
chrome: '谷歌风格',
button: '按钮风格'
}
},
header: {
title: '头部设置',
height: '头部高度',
breadcrumb: {
visible: '显示面包屑',
showIcon: '显示面包屑图标'
}
},
sider: {
title: '侧边栏设置',
inverted: '深色侧边栏',
width: '侧边栏宽度',
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
},
footer: {
title: '底部设置',
visible: '显示底部',
fixed: '固定底部',
height: '底部高度',
right: '底部局右'
},
content: {
title: '内容区域设置',
scrollMode: { scrollMode: {
title: '滚动模式', title: '滚动模式',
wrapper: '外层滚动', wrapper: '外层滚动',
@ -103,55 +157,32 @@ const local: App.I18n.Schema = {
none: '无' none: '无'
} }
}, },
fixedHeaderAndTab: '固定头部和标签栏', fixedHeaderAndTab: '固定头部和标签栏'
header: {
height: '头部高度',
breadcrumb: {
visible: '显示面包屑',
showIcon: '显示面包屑图标'
}, },
multilingual: {
visible: '显示多语言按钮'
},
globalSearch: {
visible: '显示全局搜索按钮'
}
},
tab: {
visible: '显示标签栏',
cache: '标签栏信息缓存',
height: '标签栏高度',
mode: {
title: '标签栏风格',
chrome: '谷歌风格',
button: '按钮风格'
}
},
sider: {
inverted: '深色侧边栏',
width: '侧边栏宽度',
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
},
footer: {
visible: '显示底部',
fixed: '固定底部',
height: '底部高度',
right: '底部局右'
},
watermark: {
visible: '显示全屏水印',
text: '水印文本',
enableUserName: '启用用户名水印'
},
themeDrawerTitle: '主题配置',
pageFunTitle: '页面功能',
resetCacheStrategy: { resetCacheStrategy: {
title: '重置缓存策略', title: '重置缓存策略',
close: '关闭页面', close: '关闭页面',
refresh: '刷新页面' refresh: '刷新页面'
}
},
general: {
title: '通用设置',
watermark: {
title: '水印设置',
visible: '显示全屏水印',
text: '自定义水印文本',
enableUserName: '启用用户名水印',
enableTime: '显示当前时间',
timeFormat: '时间格式'
},
multilingual: {
title: '多语言设置',
visible: '显示多语言按钮'
},
globalSearch: {
title: '全局搜索设置',
visible: '显示全局搜索按钮'
}
}, },
configOperation: { configOperation: {
copyConfig: '复制配置', copyConfig: '复制配置',

View File

@ -1,10 +1,11 @@
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue'; import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { useEventListener, usePreferredColorScheme } from '@vueuse/core'; import { useDateFormat, useEventListener, useNow, usePreferredColorScheme } from '@vueuse/core';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getPaletteColorByNumber } from '@sa/color'; import { getPaletteColorByNumber } from '@sa/color';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum'; import { SetupStoreId } from '@/enum';
import { useAuthStore } from '../auth';
import { import {
addThemeVarsToGlobal, addThemeVarsToGlobal,
createThemeToken, createThemeToken,
@ -18,10 +19,14 @@ import {
export const useThemeStore = defineStore(SetupStoreId.Theme, () => { export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
const scope = effectScope(); const scope = effectScope();
const osTheme = usePreferredColorScheme(); const osTheme = usePreferredColorScheme();
const authStore = useAuthStore();
/** Theme settings */ /** Theme settings */
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings()); const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
/** Watermark time instance with controls */
const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
/** Dark mode */ /** Dark mode */
const darkMode = computed(() => { const darkMode = computed(() => {
if (settings.value.themeScheme === 'auto') { if (settings.value.themeScheme === 'auto') {
@ -57,6 +62,28 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
*/ */
const settingsJson = computed(() => JSON.stringify(settings.value)); const settingsJson = computed(() => JSON.stringify(settings.value));
/** Watermark time date formatter */
const formattedWatermarkTime = computed(() => {
const { watermark } = settings.value;
const date = useDateFormat(watermarkTime, watermark.timeFormat);
return date.value;
});
/** Watermark content */
const watermarkContent = computed(() => {
const { watermark } = settings.value;
if (watermark.enableUserName && authStore.userInfo.userName) {
return authStore.userInfo.userName;
}
if (watermark.enableTime) {
return formattedWatermarkTime.value;
}
return watermark.text;
});
/** Reset store */ /** Reset store */
function resetStore() { function resetStore() {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -144,13 +171,43 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
); );
addThemeVarsToGlobal(themeTokens, darkThemeTokens); addThemeVarsToGlobal(themeTokens, darkThemeTokens);
} }
/** /**
* Set layout reverse horizontal mix * Set watermark enable user name
* *
* @param reverse Reverse horizontal mix * @param enable Whether to enable user name watermark
*/ */
function setLayoutReverseHorizontalMix(reverse: boolean) { function setWatermarkEnableUserName(enable: boolean) {
settings.value.layout.reverseHorizontalMix = reverse; settings.value.watermark.enableUserName = enable;
if (enable) {
settings.value.watermark.enableTime = false;
}
}
/**
* Set watermark enable time
*
* @param enable Whether to enable time watermark
*/
function setWatermarkEnableTime(enable: boolean) {
settings.value.watermark.enableTime = enable;
if (enable) {
settings.value.watermark.enableUserName = false;
}
}
/** Only run timer when watermark is visible and time display is enabled */
function updateWatermarkTimer() {
const { watermark } = settings.value;
const shouldRunTimer = watermark.visible && watermark.enableTime;
if (shouldRunTimer) {
resumeWatermarkTime();
} else {
pauseWatermarkTime();
}
} }
/** Cache theme settings */ /** Cache theme settings */
@ -196,6 +253,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
}, },
{ immediate: true } { immediate: true }
); );
// watch watermark settings to control timer
watch(
() => [settings.value.watermark.visible, settings.value.watermark.enableTime],
() => {
updateWatermarkTimer();
},
{ immediate: true }
);
}); });
/** On scope dispose */ /** On scope dispose */
@ -209,6 +275,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
themeColors, themeColors,
naiveTheme, naiveTheme,
settingsJson, settingsJson,
watermarkContent,
setGrayscale, setGrayscale,
setColourWeakness, setColourWeakness,
resetStore, resetStore,
@ -216,6 +283,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
toggleThemeScheme, toggleThemeScheme,
updateThemeColors, updateThemeColors,
setThemeLayout, setThemeLayout,
setLayoutReverseHorizontalMix setWatermarkEnableUserName,
setWatermarkEnableTime
}; };
}); });

View File

@ -15,8 +15,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
resetCacheStrategy: 'close', resetCacheStrategy: 'close',
layout: { layout: {
mode: 'vertical', mode: 'vertical',
scrollMode: 'content', scrollMode: 'content'
reverseHorizontalMix: false
}, },
page: { page: {
animate: true, animate: true,
@ -59,7 +58,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
watermark: { watermark: {
visible: false, visible: false,
text: 'SoybeanAdmin', text: 'SoybeanAdmin',
enableUserName: false enableUserName: false,
enableTime: false,
timeFormat: 'YYYY-MM-DD HH:mm'
}, },
tokens: { tokens: {
light: { light: {

95
src/typings/app.d.ts vendored
View File

@ -28,12 +28,6 @@ declare namespace App {
mode: UnionKey.ThemeLayoutMode; mode: UnionKey.ThemeLayoutMode;
/** Scroll mode */ /** Scroll mode */
scrollMode: UnionKey.ThemeScrollMode; scrollMode: UnionKey.ThemeScrollMode;
/**
* Whether to reverse the horizontal mix
*
* if true, the vertical child level menus in left and horizontal first level menus in top
*/
reverseHorizontalMix: boolean;
}; };
/** Page */ /** Page */
page: { page: {
@ -88,11 +82,14 @@ declare namespace App {
width: number; width: number;
/** Collapsed sider width */ /** Collapsed sider width */
collapsedWidth: number; collapsedWidth: number;
/** Sider width when the layout is 'vertical-mix' or 'horizontal-mix' */ /** Sider width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
mixWidth: number; mixWidth: number;
/** Collapsed sider width when the layout is 'vertical-mix' or 'horizontal-mix' */ /**
* Collapsed sider width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or
* 'top-hybrid-header-first'
*/
mixCollapsedWidth: number; mixCollapsedWidth: number;
/** Child menu width when the layout is 'vertical-mix' or 'horizontal-mix' */ /** Child menu width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
mixChildMenuWidth: number; mixChildMenuWidth: number;
}; };
/** Footer */ /** Footer */
@ -103,7 +100,10 @@ declare namespace App {
fixed: boolean; fixed: boolean;
/** Footer height */ /** Footer height */
height: number; height: number;
/** Whether float the footer to the right when the layout is 'horizontal-mix' */ /**
* Whether float the footer to the right when the layout is 'top-hybrid-sidebar-first' or
* 'top-hybrid-header-first'
*/
right: boolean; right: boolean;
}; };
/** Watermark */ /** Watermark */
@ -114,6 +114,10 @@ declare namespace App {
text: string; text: string;
/** Whether to use user name as watermark text */ /** Whether to use user name as watermark text */
enableUserName: boolean; enableUserName: boolean;
/** Whether to use current time as watermark text */
enableTime: boolean;
/** Time format for watermark text */
timeFormat: string;
}; };
/** define some theme settings tokens, will transform to css variables */ /** define some theme settings tokens, will transform to css variables */
tokens: { tokens: {
@ -358,42 +362,44 @@ declare namespace App {
tokenExpired: string; tokenExpired: string;
}; };
theme: { theme: {
themeDrawerTitle: string;
tabs: {
appearance: string;
layout: string;
general: string;
};
appearance: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>; themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
grayscale: string; grayscale: string;
colourWeakness: string; colourWeakness: string;
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
recommendColor: string;
recommendColorDesc: string;
themeColor: { themeColor: {
title: string; title: string;
followPrimary: string; followPrimary: string;
} & Theme.ThemeColor; } & Theme.ThemeColor;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>; recommendColor: string;
page: { recommendColorDesc: string;
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
header: {
height: string;
breadcrumb: {
visible: string;
showIcon: string;
};
multilingual: {
visible: string;
};
globalSearch: {
visible: string;
}; };
layout: {
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string> & {
[K in `${UnionKey.ThemeLayoutMode}_detail`]: string;
}; };
tab: { tab: {
title: string;
visible: string; visible: string;
cache: string; cache: string;
height: string; height: string;
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>; mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
}; };
header: {
title: string;
height: string;
breadcrumb: {
visible: string;
showIcon: string;
};
};
sider: { sider: {
title: string;
inverted: string; inverted: string;
width: string; width: string;
collapsedWidth: string; collapsedWidth: string;
@ -402,19 +408,42 @@ declare namespace App {
mixChildMenuWidth: string; mixChildMenuWidth: string;
}; };
footer: { footer: {
title: string;
visible: string; visible: string;
fixed: string; fixed: string;
height: string; height: string;
right: string; right: string;
}; };
content: {
title: string;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
page: {
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
};
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
};
general: {
title: string;
watermark: { watermark: {
title: string;
visible: string; visible: string;
text: string; text: string;
enableUserName: string; enableUserName: string;
enableTime: string;
timeFormat: string;
};
multilingual: {
title: string;
visible: string;
};
globalSearch: {
title: string;
visible: string;
};
}; };
themeDrawerTitle: string;
pageFunTitle: string;
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
configOperation: { configOperation: {
copyConfig: string; copyConfig: string;
copySuccessMsg: string; copySuccessMsg: string;

View File

@ -38,6 +38,8 @@ declare module 'vue' {
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default'] IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default'] IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalActivity: typeof import('~icons/local/activity')['default'] IconLocalActivity: typeof import('~icons/local/activity')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalCast: typeof import('~icons/local/cast')['default'] IconLocalCast: typeof import('~icons/local/cast')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default'] IconLocalLogo: typeof import('~icons/local/logo')['default']
@ -45,7 +47,6 @@ declare module 'vue' {
'IconMdi:printer': typeof import('~icons/mdi/printer')['default'] 'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
@ -65,20 +66,15 @@ declare module 'vue' {
NCollapse: typeof import('naive-ui')['NCollapse'] NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem'] NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker'] NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer'] NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown'] NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
@ -108,10 +104,8 @@ declare module 'vue' {
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane'] NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing'] NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NWatermark: typeof import('naive-ui')['NWatermark'] NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ProCard: typeof import('pro-naive-ui')['ProCard'] ProCard: typeof import('pro-naive-ui')['ProCard']

View File

@ -28,9 +28,16 @@ declare namespace UnionKey {
* - vertical: the vertical menu in left * - vertical: the vertical menu in left
* - horizontal: the horizontal menu in top * - horizontal: the horizontal menu in top
* - vertical-mix: two vertical mixed menus in left * - vertical-mix: two vertical mixed menus in left
* - horizontal-mix: the vertical first level menus in left and horizontal child level menus in top * - top-hybrid-sidebar-first: the vertical first level menus in left and horizontal child level menus in top
* - top-hybrid-header-first: the horizontal first level menus in top and vertical child level menus in left
*/ */
type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix'; type ThemeLayoutMode =
| 'vertical'
| 'horizontal'
| 'vertical-mix'
| 'vertical-hybrid-header-first'
| 'top-hybrid-sidebar-first'
| 'top-hybrid-header-first';
/** /**
* The scroll mode when content overflow * The scroll mode when content overflow