This commit is contained in:
dap
2026-02-26 19:16:57 +08:00
33 changed files with 268 additions and 77 deletions

View File

@@ -1,4 +1,5 @@
export * from './config'; export * from './config';
export * from './options'; export * from './options';
export * from './plugins'; export * from './plugins';
export type * from './typing';
export { loadAndConvertEnv } from './utils/env'; export { loadAndConvertEnv } from './utils/env';

View File

@@ -29,6 +29,7 @@ export {
FoldHorizontal, FoldHorizontal,
Fullscreen, Fullscreen,
Github, Github,
Grid,
Grip, Grip,
GripVertical, GripVertical,
Menu as IconDefault, Menu as IconDefault,
@@ -36,6 +37,7 @@ export {
Info, Info,
InspectionPanel, InspectionPanel,
Languages, Languages,
LayoutGrid,
LoaderCircle, LoaderCircle,
LockKeyhole, LockKeyhole,
LogOut, LogOut,

View File

@@ -1,20 +1,38 @@
import type { ComputedRef, MaybeRef } from 'vue'; import type { ComputedRef, MaybeRef } from 'vue';
/**
* 类型级递归中增加深度计数
*/
type Increment<A extends unknown[]> = [...A, unknown];
/** /**
* 深层递归所有属性为可选 * 深层递归所有属性为可选
*/ */
type DeepPartial<T> = T extends object type DeepPartial<
? { T,
[P in keyof T]?: DeepPartial<T[P]>; D extends number = 10,
} C extends unknown[] = [],
: T; > = C['length'] extends D
? T
: T extends object
? {
[P in keyof T]?: DeepPartial<T[P], D, Increment<C>>;
}
: T;
/** /**
* 深层递归所有属性为只读 * 深层递归所有属性为只读
*/ */
type DeepReadonly<T> = { type DeepReadonly<
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; T,
}; D extends number = 10,
C extends unknown[] = [],
> = C['length'] extends D
? T
: T extends object
? {
readonly [P in keyof T]: DeepReadonly<T[P], D, Increment<C>>;
}
: T;
/** /**
* 任意类型的异步函数 * 任意类型的异步函数

View File

@@ -104,6 +104,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"showIcon": true, "showIcon": true,
"showMaximize": true, "showMaximize": true,
"showMore": true, "showMore": true,
"showRefresh": true,
"styleType": "chrome", "styleType": "chrome",
"visitHistory": true, "visitHistory": true,
"wheelable": true, "wheelable": true,
@@ -119,6 +120,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"radius": "0.5", "radius": "0.5",
"semiDarkHeader": false, "semiDarkHeader": false,
"semiDarkSidebar": false, "semiDarkSidebar": false,
"semiDarkSidebarSub": false,
}, },
"transition": { "transition": {
"enable": true, "enable": true,

View File

@@ -105,6 +105,7 @@ const defaultPreferences: Preferences = {
showIcon: true, showIcon: true,
showMaximize: true, showMaximize: true,
showMore: true, showMore: true,
showRefresh: true,
styleType: 'chrome', styleType: 'chrome',
visitHistory: true, visitHistory: true,
wheelable: true, wheelable: true,
@@ -120,6 +121,7 @@ const defaultPreferences: Preferences = {
fontSize: 16, fontSize: 16,
semiDarkHeader: false, semiDarkHeader: false,
semiDarkSidebar: false, semiDarkSidebar: false,
semiDarkSidebarSub: false,
}, },
transition: { transition: {
enable: true, enable: true,

View File

@@ -38,12 +38,10 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
primaryColor: 'hsl(240 5.9% 10%)', primaryColor: 'hsl(240 5.9% 10%)',
type: 'zinc', type: 'zinc',
}, },
{ {
color: 'hsl(181 84% 32%)', color: 'hsl(181 84% 32%)',
type: 'deep-green', type: 'deep-green',
}, },
{ {
color: 'hsl(211 91% 39%)', color: 'hsl(211 91% 39%)',
type: 'deep-blue', type: 'deep-blue',
@@ -56,7 +54,6 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
color: 'hsl(0 75% 42%)', color: 'hsl(0 75% 42%)',
type: 'rose', type: 'rose',
}, },
{ {
color: 'hsl(0 0% 25%)', color: 'hsl(0 0% 25%)',
darkPrimaryColor: 'hsl(0 0% 98%)', darkPrimaryColor: 'hsl(0 0% 98%)',

View File

@@ -222,6 +222,8 @@ interface TabbarPreferences {
showMaximize: boolean; showMaximize: boolean;
/** 显示更多按钮 */ /** 显示更多按钮 */
showMore: boolean; showMore: boolean;
/** 显示刷新按钮 */
showRefresh: boolean;
/** 标签页风格 */ /** 标签页风格 */
styleType: TabsStyleType; styleType: TabsStyleType;
/** 是否开启访问历史记录 */ /** 是否开启访问历史记录 */
@@ -251,6 +253,8 @@ interface ThemePreferences {
semiDarkHeader: boolean; semiDarkHeader: boolean;
/** 是否开启半深色菜单只在theme='light'时生效) */ /** 是否开启半深色菜单只在theme='light'时生效) */
semiDarkSidebar: boolean; semiDarkSidebar: boolean;
/** 是否开启半深色子菜单只在theme='light'时生效) */
semiDarkSidebarSub: boolean;
} }
interface TransitionPreferences { interface TransitionPreferences {

View File

@@ -381,10 +381,10 @@ onUnmounted(() => {
<div v-if="suffix" class="ml-1"> <div v-if="suffix" class="ml-1">
<VbenRenderContent :content="suffix" /> <VbenRenderContent :content="suffix" />
</div> </div>
<FormDescription v-if="description" class="ml-1">
<VbenRenderContent :content="description" />
</FormDescription>
</div> </div>
<FormDescription v-if="description" class="text-xs">
<VbenRenderContent :content="description" />
</FormDescription>
<Transition name="slide-up" v-if="!compact"> <Transition name="slide-up" v-if="!compact">
<FormMessage class="absolute bottom-[-4px]" /> <FormMessage class="absolute bottom-[-4px]" />

View File

@@ -77,7 +77,10 @@ interface Props {
* 主题 * 主题
*/ */
theme: string; theme: string;
/**
* 子主题
*/
themeSub: string;
/** /**
* 宽度 * 宽度
*/ */
@@ -261,40 +264,48 @@ function handleMouseleave() {
class="h-full transition-all duration-150" class="h-full transition-all duration-150"
></div> ></div>
<aside <aside
:class="[
theme,
{
'bg-sidebar-deep': isSidebarMixed,
'border-r border-border bg-sidebar': !isSidebarMixed,
},
]"
:style="style" :style="style"
class="fixed left-0 top-0 h-full transition-all duration-150" class="fixed left-0 top-0 h-full transition-all duration-150"
@mouseenter="handleMouseenter" @mouseenter="handleMouseenter"
@mouseleave="handleMouseleave" @mouseleave="handleMouseleave"
> >
<SidebarFixedButton <div
v-if="!collapse && !isSidebarMixed && showFixedButton" class="h-full"
v-model:expand-on-hover="expandOnHover" :class="[
/> theme,
<div v-if="slots.logo" :style="headerStyle"> {
<slot name="logo"></slot> 'bg-sidebar-deep': isSidebarMixed,
</div> 'border-r border-border bg-sidebar': !isSidebarMixed,
<VbenScrollbar :style="contentStyle" shadow shadow-border> },
<slot></slot> ]"
</VbenScrollbar> :style="{ width: `${width}px` }"
>
<SidebarFixedButton
v-if="!collapse && !isSidebarMixed && showFixedButton"
v-model:expand-on-hover="expandOnHover"
/>
<div v-if="slots.logo" :style="headerStyle">
<slot name="logo"></slot>
</div>
<VbenScrollbar :style="contentStyle" shadow shadow-border>
<slot></slot>
</VbenScrollbar>
<div :style="collapseStyle"></div> <div :style="collapseStyle"></div>
<SidebarCollapseButton <SidebarCollapseButton
v-if="showCollapseButton && !isSidebarMixed" v-if="showCollapseButton && !isSidebarMixed"
v-model:collapsed="collapse" v-model:collapsed="collapse"
/> />
</div>
<div <div
v-if="isSidebarMixed" v-if="isSidebarMixed"
ref="asideRef" ref="asideRef"
:class="{ :class="[
'border-l': extraVisible, themeSub,
}" {
'border-l': extraVisible,
},
]"
:style="extraStyle" :style="extraStyle"
class="fixed top-0 h-full overflow-hidden border-r border-border bg-sidebar transition-all duration-200" class="fixed top-0 h-full overflow-hidden border-r border-border bg-sidebar transition-all duration-200"
> >

View File

@@ -146,6 +146,11 @@ interface VbenLayoutProps {
* @default dark * @default dark
*/ */
sidebarTheme?: ThemeModeType; sidebarTheme?: ThemeModeType;
/**
* 侧边栏子栏
* @default dark
*/
sidebarThemeSub?: ThemeModeType;
/** /**
* 侧边栏宽度 * 侧边栏宽度
* @default 210 * @default 210

View File

@@ -56,6 +56,7 @@ const props = withDefaults(defineProps<Props>(), {
sidebarHidden: false, sidebarHidden: false,
sidebarMixedWidth: 80, sidebarMixedWidth: 80,
sidebarTheme: 'dark', sidebarTheme: 'dark',
sidebarThemeSub: 'dark',
sidebarWidth: 180, sidebarWidth: 180,
sideCollapseWidth: 60, sideCollapseWidth: 60,
tabbarEnable: true, tabbarEnable: true,
@@ -502,6 +503,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:mixed-width="sidebarMixedWidth" :mixed-width="sidebarMixedWidth"
:show="showSidebar" :show="showSidebar"
:theme="sidebarTheme" :theme="sidebarTheme"
:theme-sub="sidebarThemeSub"
:width="getSidebarWidth" :width="getSidebarWidth"
:z-index="sidebarZIndex" :z-index="sidebarZIndex"
@leave="() => emit('sideMouseLeave')" @leave="() => emit('sideMouseLeave')"

View File

@@ -463,33 +463,33 @@ $namespace: vben;
&.is-dark { &.is-dark {
--menu-background-color: hsl(var(--menu)); --menu-background-color: hsl(var(--menu));
// --menu-submenu-opened-background-color: hsl(var(--menu-opened-dark)); // --menu-submenu-opened-background-color: hsl(var(--menu-opened-dark));
--menu-item-background-color: var(--menu-background-color);
--menu-item-color: hsl(var(--foreground) / 80%); --menu-item-color: hsl(var(--foreground) / 80%);
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--accent-foreground)); --menu-item-hover-color: hsl(var(--accent-foreground));
--menu-item-hover-background-color: hsl(var(--accent)); --menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--accent-foreground)); --menu-item-active-color: hsl(var(--accent-foreground));
--menu-item-active-background-color: hsl(var(--accent)); --menu-item-active-background-color: hsl(var(--accent));
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--foreground));
--menu-submenu-active-background-color: transparent;
--menu-submenu-background-color: var(--menu-background-color); --menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--accent-foreground));
--menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--accent-foreground));
--menu-submenu-active-background-color: transparent;
} }
&.is-light { &.is-light {
--menu-background-color: hsl(var(--menu)); --menu-background-color: hsl(var(--menu));
// --menu-submenu-opened-background-color: hsl(var(--menu-opened)); // --menu-submenu-opened-background-color: hsl(var(--menu-opened));
--menu-item-color: hsl(var(--accent-foreground));
--menu-item-background-color: var(--menu-background-color); --menu-item-background-color: var(--menu-background-color);
--menu-item-color: hsl(var(--foreground));
--menu-item-hover-color: var(--menu-item-color); --menu-item-hover-color: var(--menu-item-color);
--menu-item-hover-background-color: hsl(var(--accent)); --menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--primary)); --menu-item-active-color: hsl(var(--primary));
--menu-item-active-background-color: hsl(var(--primary) / 15%); --menu-item-active-background-color: hsl(var(--primary) / 15%);
--menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-hover-color: hsl(var(--primary)); --menu-submenu-hover-color: hsl(var(--primary));
--menu-submenu-hover-background-color: hsl(var(--accent)); --menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--primary)); --menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: transparent; --menu-submenu-active-background-color: transparent;
--menu-submenu-background-color: var(--menu-background-color);
} }
&.is-rounded { &.is-rounded {
@@ -518,25 +518,33 @@ $namespace: vben;
--menu-background-color: transparent; --menu-background-color: transparent;
&.is-dark { &.is-dark {
--menu-background-color: hsl(var(--menu));
--menu-item-color: hsl(var(--foreground) / 80%);
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--accent-foreground)); --menu-item-hover-color: hsl(var(--accent-foreground));
--menu-item-hover-background-color: hsl(var(--accent)); --menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--accent-foreground)); --menu-item-active-color: hsl(var(--accent-foreground));
--menu-item-active-background-color: hsl(var(--accent)); --menu-item-active-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--foreground)); --menu-submenu-background-color: var(--menu-background-color);
--menu-submenu-active-background-color: hsl(var(--accent));
--menu-submenu-hover-color: hsl(var(--accent-foreground)); --menu-submenu-hover-color: hsl(var(--accent-foreground));
--menu-submenu-hover-background-color: hsl(var(--accent)); --menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--accent-foreground));
--menu-submenu-active-background-color: hsl(var(--accent));
} }
&.is-light { &.is-light {
--menu-background-color: hsl(var(--menu));
--menu-item-color: hsl(var(--accent-foreground));
--menu-item-background-color: var(--menu-background-color);
--menu-item-hover-color: var(--menu-item-color);
--menu-item-hover-background-color: hsl(var(--accent));
--menu-item-active-color: hsl(var(--primary)); --menu-item-active-color: hsl(var(--primary));
--menu-item-active-background-color: hsl(var(--primary) / 15%); --menu-item-active-background-color: hsl(var(--primary) / 15%);
--menu-item-hover-background-color: hsl(var(--accent)); --menu-submenu-background-color: var(--menu-background-color);
--menu-item-hover-color: hsl(var(--primary));
--menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
--menu-submenu-hover-color: hsl(var(--primary)); --menu-submenu-hover-color: hsl(var(--primary));
--menu-submenu-hover-background-color: hsl(var(--accent)); --menu-submenu-hover-background-color: hsl(var(--accent));
--menu-submenu-active-color: hsl(var(--primary));
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
} }
} }
} }
@@ -862,9 +870,8 @@ $namespace: vben;
padding-right: 12px !important; padding-right: 12px !important;
} }
// &:not(.is-active):hover { &:not(.is-active):hover {
&:hover { //color: var(--menu-submenu-hover-color);
color: var(--menu-submenu-hover-color);
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
background: var(--menu-submenu-hover-background-color) !important; background: var(--menu-submenu-hover-background-color) !important;

View File

@@ -151,10 +151,12 @@ $namespace: vben;
} }
&__name { &__name {
width: 100%;
margin-top: 8px; margin-top: 8px;
margin-bottom: 0; margin-bottom: 0;
font-size: calc(var(--font-size-base, 16px) * 0.75); font-size: calc(var(--font-size-base, 16px) * 0.75);
font-weight: 400; font-weight: 400;
text-align: center;
transition: all 0.25s ease; transition: all 0.25s ease;
} }
} }

View File

@@ -210,6 +210,7 @@ onBeforeUnmount(() => {
opened ? '' : 'hidden', opened ? '' : 'hidden',
'overflow-auto', 'overflow-auto',
'max-h-[calc(var(--reka-hover-card-content-available-height)-20px)]', 'max-h-[calc(var(--reka-hover-card-content-available-height)-20px)]',
mode === 'horizontal' ? 'is-horizontal' : '',
]" ]"
:content-props="contentProps" :content-props="contentProps"
:open="true" :open="true"

View File

@@ -158,7 +158,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<VbenIcon <VbenIcon
v-if="showIcon" v-if="showIcon"
:icon="tab.icon" :icon="tab.icon"
class="mr-1 flex size-4 items-center overflow-hidden" class="mr-1 flex size-4 items-center overflow-hidden group-hover:animate-[shrink_0.3s_ease-in-out]"
/> />
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm"> <span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
{{ tab.title }} {{ tab.title }}

View File

@@ -132,7 +132,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<VbenIcon <VbenIcon
v-if="showIcon" v-if="showIcon"
:icon="tab.icon" :icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden" class="mr-2 flex size-4 items-center overflow-hidden group-hover:animate-[shrink_0.3s_ease-in-out]"
fallback fallback
/> />

View File

@@ -1,2 +1,3 @@
export { default as TabsToolMore } from './tool-more.vue'; export { default as TabsToolMore } from './tool-more.vue';
export { default as TabsToolRefresh } from './tool-refresh.vue';
export { default as TabsToolScreen } from './tool-screen.vue'; export { default as TabsToolScreen } from './tool-screen.vue';

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DropdownMenuProps } from '@vben-core/shadcn-ui'; import type { DropdownMenuProps } from '@vben-core/shadcn-ui';
import { ChevronDown } from '@vben-core/icons'; import { LayoutGrid } from '@vben-core/icons';
import { VbenDropdownMenu } from '@vben-core/shadcn-ui'; import { VbenDropdownMenu } from '@vben-core/shadcn-ui';
defineProps<DropdownMenuProps>(); defineProps<DropdownMenuProps>();
@@ -12,7 +12,7 @@ defineProps<DropdownMenuProps>();
<div <div
class="flex-center h-full cursor-pointer border-l border-border px-2 text-lg font-semibold text-muted-foreground hover:bg-muted hover:text-foreground" class="flex-center h-full cursor-pointer border-l border-border px-2 text-lg font-semibold text-muted-foreground hover:bg-muted hover:text-foreground"
> >
<ChevronDown class="size-4" /> <LayoutGrid class="size-4" />
</div> </div>
</VbenDropdownMenu> </VbenDropdownMenu>
</template> </template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { RotateCw } from '@vben-core/icons';
const emit = defineEmits(['refresh']);
const handleRefresh = () => {
emit('refresh');
};
</script>
<template>
<div
class="flex-center h-full cursor-pointer border-l border-border px-2 text-lg font-semibold text-muted-foreground hover:bg-muted hover:text-foreground"
@click="handleRefresh"
>
<RotateCw class="size-4" />
</div>
</template>

View File

@@ -2,7 +2,7 @@
import type { TabsEmits, TabsProps } from './types'; import type { TabsEmits, TabsProps } from './types';
import { useForwardPropsEmits } from '@vben-core/composables'; import { useForwardPropsEmits } from '@vben-core/composables';
import { ChevronLeft, ChevronRight } from '@vben-core/icons'; import { ChevronsLeft, ChevronsRight } from '@vben-core/icons';
import { VbenScrollbar } from '@vben-core/shadcn-ui'; import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { Tabs, TabsChrome } from './components'; import { Tabs, TabsChrome } from './components';
@@ -60,7 +60,7 @@ useTabsDrag(props, emit);
class="border-r px-2" class="border-r px-2"
@click="scrollDirection('left')" @click="scrollDirection('left')"
> >
<ChevronLeft class="size-4 h-full" /> <ChevronsLeft class="size-4 h-full" />
</span> </span>
<div <div
@@ -101,7 +101,7 @@ useTabsDrag(props, emit);
class="cursor-pointer border-l px-2 text-muted-foreground hover:bg-muted" class="cursor-pointer border-l px-2 text-muted-foreground hover:bg-muted"
@click="scrollDirection('right')" @click="scrollDirection('right')"
> >
<ChevronRight class="size-4 h-full" /> <ChevronsRight class="size-4 h-full" />
</span> </span>
</div> </div>
</template> </template>

View File

@@ -60,6 +60,11 @@ const sidebarTheme = computed(() => {
return dark ? 'dark' : 'light'; return dark ? 'dark' : 'light';
}); });
const sidebarThemeSub = computed(() => {
const dark = isDark.value || preferences.theme.semiDarkSidebarSub;
return dark ? 'dark' : 'light';
});
const headerTheme = computed(() => { const headerTheme = computed(() => {
const dark = isDark.value || preferences.theme.semiDarkHeader; const dark = isDark.value || preferences.theme.semiDarkHeader;
return dark ? 'dark' : 'light'; return dark ? 'dark' : 'light';
@@ -240,6 +245,7 @@ const headerSlots = computed(() => {
:sidebar-hidden="preferences.sidebar.hidden" :sidebar-hidden="preferences.sidebar.hidden"
:sidebar-mixed-width="preferences.sidebar.mixedWidth" :sidebar-mixed-width="preferences.sidebar.mixedWidth"
:sidebar-theme="sidebarTheme" :sidebar-theme="sidebarTheme"
:sidebar-theme-sub="sidebarThemeSub"
:sidebar-width="preferences.sidebar.width" :sidebar-width="preferences.sidebar.width"
:side-collapse-width="preferences.sidebar.collapseWidth" :side-collapse-width="preferences.sidebar.collapseWidth"
:tabbar-enable="preferences.tabbar.enable" :tabbar-enable="preferences.tabbar.enable"
@@ -355,7 +361,7 @@ const headerSlots = computed(() => {
:collapse="preferences.sidebar.extraCollapse" :collapse="preferences.sidebar.extraCollapse"
:menus="wrapperMenus(extraMenus)" :menus="wrapperMenus(extraMenus)"
:rounded="isMenuRounded" :rounded="isMenuRounded"
:theme="sidebarTheme" :theme="sidebarThemeSub"
/> />
</template> </template>
<template #side-extra-title> <template #side-extra-title>
@@ -411,7 +417,7 @@ const headerSlots = computed(() => {
<template v-if="preferencesButtonPosition.fixed"> <template v-if="preferencesButtonPosition.fixed">
<Preferences <Preferences
class="z-100 fixed bottom-20 right-0" class="z-100 fixed right-0 top-1/2 -translate-y-1/2 transform"
@clear-preferences-and-logout="clearPreferencesAndLogout" @clear-preferences-and-logout="clearPreferencesAndLogout"
/> />
</template> </template>

View File

@@ -6,7 +6,12 @@ import { useContentMaximize, useTabs } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useTabbarStore } from '@vben/stores'; import { useTabbarStore } from '@vben/stores';
import { TabsToolMore, TabsToolScreen, TabsView } from '@vben-core/tabs-ui'; import {
TabsToolMore,
TabsToolRefresh,
TabsToolScreen,
TabsView,
} from '@vben-core/tabs-ui';
import { useTabbar } from './use-tabbar'; import { useTabbar } from './use-tabbar';
@@ -19,7 +24,7 @@ defineProps<{ showIcon?: boolean; theme?: string }>();
const route = useRoute(); const route = useRoute();
const tabbarStore = useTabbarStore(); const tabbarStore = useTabbarStore();
const { contentIsMaximize, toggleMaximize } = useContentMaximize(); const { contentIsMaximize, toggleMaximize } = useContentMaximize();
const { unpinTab } = useTabs(); const { refreshTab, unpinTab } = useTabs();
const { const {
createContextMenus, createContextMenus,
@@ -65,6 +70,10 @@ if (!preferences.tabbar.persist) {
/> />
<div class="flex-center h-full"> <div class="flex-center h-full">
<TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" /> <TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" />
<TabsToolRefresh
v-if="preferences.tabbar.showRefresh"
@refresh="refreshTab"
/>
<TabsToolScreen <TabsToolScreen
v-if="preferences.tabbar.showMaximize" v-if="preferences.tabbar.showMaximize"
:screen="contentIsMaximize" :screen="contentIsMaximize"

View File

@@ -58,7 +58,6 @@ function typeView(name: BuiltinThemeType) {
case 'green': { case 'green': {
return $t('preferences.theme.builtin.green'); return $t('preferences.theme.builtin.green');
} }
case 'neutral': { case 'neutral': {
return $t('preferences.theme.builtin.neutral'); return $t('preferences.theme.builtin.neutral');
} }

View File

@@ -2,8 +2,11 @@
import type { ThemeModeType } from '@vben/types'; import type { ThemeModeType } from '@vben/types';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { watch } from 'vue';
import { MoonStar, Sun, SunMoon } from '@vben/icons'; import { MoonStar, Sun, SunMoon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { usePreferences } from '@vben/preferences';
import SwitchItem from '../switch-item.vue'; import SwitchItem from '../switch-item.vue';
@@ -13,8 +16,20 @@ defineOptions({
const modelValue = defineModel<string>({ default: 'auto' }); const modelValue = defineModel<string>({ default: 'auto' });
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar'); const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
const themeSemiDarkSidebarSub = defineModel<boolean>('themeSemiDarkSidebarSub');
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader'); const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
const { layout } = usePreferences();
watch(
() => themeSemiDarkSidebar.value,
() => {
if (!themeSemiDarkSidebar.value) {
themeSemiDarkSidebarSub.value = themeSemiDarkSidebar.value;
}
},
);
const THEME_PRESET: Array<{ icon: Component; name: ThemeModeType }> = [ const THEME_PRESET: Array<{ icon: Component; name: ThemeModeType }> = [
{ {
icon: Sun, icon: Sun,
@@ -70,11 +85,27 @@ function nameView(name: string) {
<SwitchItem <SwitchItem
v-model="themeSemiDarkSidebar" v-model="themeSemiDarkSidebar"
:disabled="modelValue === 'dark'" :disabled="
modelValue === 'dark' ||
layout === 'header-nav' ||
layout === 'full-content'
"
:tip="$t('preferences.theme.darkSidebarTip')"
class="mt-6" class="mt-6"
> >
{{ $t('preferences.theme.darkSidebar') }} {{ $t('preferences.theme.darkSidebar') }}
</SwitchItem> </SwitchItem>
<SwitchItem
v-model="themeSemiDarkSidebarSub"
:disabled="
modelValue === 'dark' ||
(layout !== 'header-mixed-nav' && layout !== 'sidebar-mixed-nav') ||
!themeSemiDarkSidebar
"
:tip="$t('preferences.theme.darkSidebarSubTip')"
>
{{ $t('preferences.theme.darkSidebarSub') }}
</SwitchItem>
<SwitchItem v-model="themeSemiDarkHeader" :disabled="modelValue === 'dark'"> <SwitchItem v-model="themeSemiDarkHeader" :disabled="modelValue === 'dark'">
{{ $t('preferences.theme.darkHeader') }} {{ $t('preferences.theme.darkHeader') }}
</SwitchItem> </SwitchItem>

View File

@@ -88,6 +88,7 @@ const themeMode = defineModel<ThemeModeType>('themeMode');
const themeRadius = defineModel<string>('themeRadius'); const themeRadius = defineModel<string>('themeRadius');
const themeFontSize = defineModel<number>('themeFontSize'); const themeFontSize = defineModel<number>('themeFontSize');
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar'); const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
const themeSemiDarkSidebarSub = defineModel<boolean>('themeSemiDarkSidebarSub');
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader'); const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
const sidebarEnable = defineModel<boolean>('sidebarEnable'); const sidebarEnable = defineModel<boolean>('sidebarEnable');
@@ -319,6 +320,7 @@ async function handleReset() {
v-model="themeMode" v-model="themeMode"
v-model:theme-semi-dark-header="themeSemiDarkHeader" v-model:theme-semi-dark-header="themeSemiDarkHeader"
v-model:theme-semi-dark-sidebar="themeSemiDarkSidebar" v-model:theme-semi-dark-sidebar="themeSemiDarkSidebar"
v-model:theme-semi-dark-sidebar-sub="themeSemiDarkSidebarSub"
/> />
</Block> </Block>
<Block :title="$t('preferences.theme.builtin.title')"> <Block :title="$t('preferences.theme.builtin.title')">

View File

@@ -6,7 +6,17 @@ import type { Nullable } from '@vben/types';
import type EchartsUI from './echarts-ui.vue'; import type EchartsUI from './echarts-ui.vue';
import { computed, nextTick, watch } from 'vue'; import {
computed,
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
ref,
unref,
watch,
} from 'vue';
import { usePreferences } from '@vben/preferences'; import { usePreferences } from '@vben/preferences';
@@ -27,6 +37,8 @@ type EchartsThemeType = 'dark' | 'light' | null;
function useEcharts(chartRef: Ref<EchartsUIType>) { function useEcharts(chartRef: Ref<EchartsUIType>) {
let chartInstance: echarts.ECharts | null = null; let chartInstance: echarts.ECharts | null = null;
let cacheOptions: EChartsOption = {}; let cacheOptions: EChartsOption = {};
// echart是否处于激活状态
const isActiveRef = ref(false);
const { isDark } = usePreferences(); const { isDark } = usePreferences();
const { height, width } = useWindowSize(); const { height, width } = useWindowSize();
@@ -42,6 +54,11 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return maybeComponent.$el ?? null; return maybeComponent.$el ?? null;
}; };
onMounted(() => (isActiveRef.value = true));
onActivated(() => (isActiveRef.value = true));
onDeactivated(() => (isActiveRef.value = false));
onBeforeUnmount(() => (isActiveRef.value = false));
const isElHidden = (el: HTMLElement | null): boolean => { const isElHidden = (el: HTMLElement | null): boolean => {
if (!el) return true; if (!el) return true;
return el.offsetHeight === 0 || el.offsetWidth === 0; return el.offsetHeight === 0 || el.offsetWidth === 0;
@@ -71,6 +88,9 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
options: EChartsOption, options: EChartsOption,
clear = true, clear = true,
): Promise<Nullable<echarts.ECharts>> => { ): Promise<Nullable<echarts.ECharts>> => {
if (!unref(isActiveRef)) {
return Promise.resolve(null);
}
cacheOptions = options; cacheOptions = options;
const currentOptions = { const currentOptions = {
...options, ...options,
@@ -105,7 +125,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
}); });
}; };
const updateDate = ( const updateData = (
option: EChartsOption, option: EChartsOption,
notMerge = false, // false = 合并保留动画true = 完全替换 notMerge = false, // false = 合并保留动画true = 完全替换
lazyUpdate = false, // true 时不立即重绘,适合短时间内多次调用 lazyUpdate = false, // true 时不立即重绘,适合短时间内多次调用
@@ -156,8 +176,8 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
useResizeObserver(chartRef as never, resizeHandler); useResizeObserver(chartRef as never, resizeHandler);
watch(isDark, () => { watch([isDark, isActiveRef], () => {
if (chartInstance) { if (chartInstance && unref(isActiveRef)) {
chartInstance.dispose(); chartInstance.dispose();
initCharts(); initCharts();
renderEcharts(cacheOptions); renderEcharts(cacheOptions);
@@ -170,9 +190,10 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
chartInstance?.dispose(); chartInstance?.dispose();
}); });
return { return {
isActive: isActiveRef,
renderEcharts, renderEcharts,
resize, resize,
updateDate, updateData,
getChartInstance: () => chartInstance, getChartInstance: () => chartInstance,
}; };
} }

View File

@@ -384,9 +384,11 @@ onUnmounted(() => {
<!-- 左侧操作区域或者title --> <!-- 左侧操作区域或者title -->
<template v-if="showToolbar" #toolbar-actions="slotProps"> <template v-if="showToolbar" #toolbar-actions="slotProps">
<slot v-if="showTableTitle" name="table-title"> <slot v-if="showTableTitle" name="table-title">
<div class="mr-1 pl-1 text-[1rem]"> <div
class="flex items-center justify-center gap-1 text-[1rem] font-bold"
>
{{ tableTitle }} {{ tableTitle }}
<VbenHelpTooltip v-if="tableTitleHelp" trigger-class="pb-1"> <VbenHelpTooltip v-if="tableTitleHelp">
{{ tableTitleHelp }} {{ tableTitleHelp }}
</VbenHelpTooltip> </VbenHelpTooltip>
</div> </div>

View File

@@ -127,6 +127,9 @@
"light": "Light", "light": "Light",
"dark": "Dark", "dark": "Dark",
"darkSidebar": "Semi Dark Sidebar", "darkSidebar": "Semi Dark Sidebar",
"darkSidebarTip": "It can be enabled when the theme is light, and the layout is neither 'Horizontal' nor 'Full Content'.",
"darkSidebarSub": "Semi Dark Sidebar Sub",
"darkSidebarSubTip": "It can be enabled when the theme is light, the semi dark sidebar is enabled, and the layout uses 'Two-Column' menu mode.",
"darkHeader": "Semi Dark Header", "darkHeader": "Semi Dark Header",
"weakMode": "Weak Mode", "weakMode": "Weak Mode",
"grayMode": "Gray Mode", "grayMode": "Gray Mode",

View File

@@ -127,6 +127,9 @@
"light": "浅色", "light": "浅色",
"dark": "深色", "dark": "深色",
"darkSidebar": "深色侧边栏", "darkSidebar": "深色侧边栏",
"darkSidebarTip": "当主题为浅色,布局不为水平菜单或不为内容全屏时可开启",
"darkSidebarSub": "深色侧边栏子栏",
"darkSidebarSubTip": "当主题为浅色,开启深色侧边栏且布局使用双列菜单模式时可开启",
"darkHeader": "深色顶栏", "darkHeader": "深色顶栏",
"weakMode": "色弱模式", "weakMode": "色弱模式",
"grayMode": "灰色模式", "grayMode": "灰色模式",

View File

@@ -583,6 +583,23 @@ export const useTabbarStore = defineStore('core-tabbar', {
{ {
pick: ['tabs', 'visitHistory'], pick: ['tabs', 'visitHistory'],
storage: sessionStorage, storage: sessionStorage,
serializer: {
serialize: JSON.stringify,
deserialize(value: string) {
const parsed = JSON.parse(value);
// Stack 类实例经 JSON 序列化后会变成普通对象 {dedup, items, maxSize}
// 丢失所有方法和 getter需要重新构建 Stack 实例
if (parsed.visitHistory && !(parsed.visitHistory instanceof Stack)) {
const raw = parsed.visitHistory;
const stack = createStack<string>(true, MAX_VISIT_HISTORY);
if (Array.isArray(raw.items)) {
stack.push(...raw.items);
}
parsed.visitHistory = stack;
}
return parsed;
},
},
}, },
], ],
state: (): TabbarState => ({ state: (): TabbarState => ({

View File

@@ -20,6 +20,7 @@ const routes: RouteRecordRaw[] = [
affixTab: true, affixTab: true,
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'), title: $t('page.dashboard.analytics'),
keepAlive: true,
}, },
}, },
{ {

View File

@@ -2,6 +2,7 @@ import type { RouteRecordRaw } from 'vue-router';
import { import {
VBEN_ANT_PREVIEW_URL, VBEN_ANT_PREVIEW_URL,
VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL, VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL, VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL, VBEN_GITHUB_URL,
@@ -9,7 +10,11 @@ import {
VBEN_NAIVE_PREVIEW_URL, VBEN_NAIVE_PREVIEW_URL,
VBEN_TD_PREVIEW_URL, VBEN_TD_PREVIEW_URL,
} from '@vben/constants'; } from '@vben/constants';
import { SvgAntdvLogoIcon, SvgTDesignIcon } from '@vben/icons'; import {
SvgAntdvLogoIcon,
SvgAntdvNextLogoIcon,
SvgTDesignIcon,
} from '@vben/icons';
import { IFrameView } from '#/layouts'; import { IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
@@ -56,6 +61,18 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.antdv'), title: $t('demos.vben.antdv'),
}, },
}, },
{
name: 'VbenAntdVNext',
path: '/vben-admin/antdv-next',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgAntdvNextLogoIcon,
link: VBEN_ANTDV_NEXT_PREVIEW_URL,
title: $t('demos.vben.antdv-next'),
},
},
{ {
name: 'VbenNaive', name: 'VbenNaive',
path: '/vben-admin/naive', path: '/vben-admin/naive',

View File

@@ -67,6 +67,13 @@ const [BaseForm, baseFormApi] = useVbenForm({
label: '字符串', label: '字符串',
rules: 'required', rules: 'required',
}, },
{
component: 'Input',
fieldName: 'desc',
// 界面显示的description
description: '这是表单描述',
label: '字符串(带描述)',
},
{ {
// 组件需要在 #/adapter.ts内注册并加上类型 // 组件需要在 #/adapter.ts内注册并加上类型
component: 'ApiSelect', component: 'ApiSelect',