feat: restore original admin shell quality

This commit is contained in:
2026-06-11 20:10:19 +08:00
parent 1fba0aafb2
commit ff024af5b5
8 changed files with 211 additions and 20 deletions
+7 -1
View File
@@ -27,6 +27,9 @@ const UserDropdown = defineAsyncComponent(() =>
(module) => module.UserDropdown, (module) => module.UserDropdown,
), ),
); );
const LockScreen = defineAsyncComponent(() =>
import('@vben/layouts/es/lock-screen').then((module) => module.LockScreen),
);
const NotificationPopup = defineAsyncComponent( const NotificationPopup = defineAsyncComponent(
() => import('#/components/notification/NotificationPopup.vue'), () => import('#/components/notification/NotificationPopup.vue'),
); );
@@ -227,7 +230,7 @@ watch(
</script> </script>
<template> <template>
<BasicLayout> <BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown> <template #user-dropdown>
<UserDropdown <UserDropdown
:avatar :avatar
@@ -286,5 +289,8 @@ watch(
<LoginForm /> <LoginForm />
</AuthenticationLoginExpiredModal> </AuthenticationLoginExpiredModal>
</template> </template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout> </BasicLayout>
</template> </template>
+5 -2
View File
@@ -20,11 +20,11 @@ export const overridesPreferences = defineOverridesPreferences({
layout: 'header-sidebar-nav', layout: 'header-sidebar-nav',
}, },
shortcutKeys: { shortcutKeys: {
enable: false, enable: true,
globalLockScreen: false, globalLockScreen: false,
globalLogout: true, globalLogout: true,
globalPreferences: false, globalPreferences: false,
globalSearch: false, globalSearch: true,
}, },
theme: { theme: {
mode: 'light', mode: 'light',
@@ -46,6 +46,9 @@ export const overridesPreferences = defineOverridesPreferences({
fit: 'contain', fit: 'contain',
source: Logo, source: Logo,
}, },
tabbar: {
persist: false,
},
widget: { widget: {
fullscreen: true, fullscreen: true,
globalSearch: true, globalSearch: true,
+19 -6
View File
@@ -12,6 +12,16 @@ import { useAppContextStore } from '#/store/app-context';
import { normalizeHomePath } from '#/utils/legacy-home'; import { normalizeHomePath } from '#/utils/legacy-home';
import { generateAccess } from './access'; import { generateAccess } from './access';
import { isLightTabRoute } from './light-menu';
function filterLightTabHistory<T extends { tabs?: unknown[] }>(history: T) {
const tabs = Array.isArray(history.tabs) ? history.tabs : [];
return {
...history,
cachedTabs: [],
tabs: tabs.filter((tab) => isLightTabRoute(tab as any)),
};
}
/** /**
* 通用守卫配置 * 通用守卫配置
@@ -39,10 +49,10 @@ function setupCommonGuard(router: Router) {
// 保存当前应用的 tab 历史到内存 // 保存当前应用的 tab 历史到内存
const currentAppKey = currentAppCode || 'main'; const currentAppKey = currentAppCode || 'main';
tabHistoryByApp.set(currentAppKey, { tabHistoryByApp.set(currentAppKey, filterLightTabHistory({
tabs: [...tabbarStore.tabs], tabs: [...tabbarStore.tabs],
cachedTabs: new Set(tabbarStore.cachedTabs), cachedTabs: new Set(tabbarStore.cachedTabs),
}); }));
// 从 sessionStorage 加载新应用的 tab 历史 // 从 sessionStorage 加载新应用的 tab 历史
const storageKey = `vben-admin-tabs-${appKey}`; const storageKey = `vben-admin-tabs-${appKey}`;
@@ -51,8 +61,9 @@ function setupCommonGuard(router: Router) {
if (savedHistory) { if (savedHistory) {
try { try {
const parsed = JSON.parse(savedHistory); const parsed = JSON.parse(savedHistory);
tabbarStore.tabs = parsed.tabs || []; const lightHistory = filterLightTabHistory(parsed);
tabbarStore.cachedTabs = new Set(parsed.cachedTabs || []); tabbarStore.tabs = lightHistory.tabs || [];
tabbarStore.cachedTabs = new Set(lightHistory.cachedTabs || []);
} catch (error) { } catch (error) {
console.error('Failed to parse tab history:', error); console.error('Failed to parse tab history:', error);
tabbarStore.tabs = []; tabbarStore.tabs = [];
@@ -87,10 +98,12 @@ function setupCommonGuard(router: Router) {
const storageKey = `vben-admin-tabs-${appKey}`; const storageKey = `vben-admin-tabs-${appKey}`;
const tabbarStore = useTabbarStore(); const tabbarStore = useTabbarStore();
const tabHistory = { const tabHistory = filterLightTabHistory({
tabs: tabbarStore.tabs, tabs: tabbarStore.tabs,
cachedTabs: [...tabbarStore.cachedTabs], cachedTabs: [...tabbarStore.cachedTabs],
}; });
tabbarStore.tabs = tabHistory.tabs as any;
tabbarStore.cachedTabs = new Set(tabHistory.cachedTabs as string[]);
sessionStorage.setItem(storageKey, JSON.stringify(tabHistory)); sessionStorage.setItem(storageKey, JSON.stringify(tabHistory));
// 关闭页面加载进度条 // 关闭页面加载进度条
+55
View File
@@ -31,10 +31,65 @@ export const LIGHT_MENU_NAMES = new Set([
'userSettings', 'userSettings',
]); ]);
const LIGHT_ROUTE_PATH_PREFIXES = [
'/account-settings',
'/ai-platform/agent',
'/ai-platform/knowledge',
'/ai-platform/model',
'/ai-platform/workflow',
'/ai-platform/workflow-runs',
'/announcement',
'/codex',
'/dept',
'/dict',
'/file-manager',
'/login-log',
'/menu',
'/message',
'/page-render/main_home',
'/permission',
'/post',
'/role',
'/server-monitor',
'/system-config',
'/ui-config',
'/user',
];
export function isLightMenuName(name?: string) { export function isLightMenuName(name?: string) {
return !name || LIGHT_MENU_NAMES.has(name); return !name || LIGHT_MENU_NAMES.has(name);
} }
function normalizePath(path?: string) {
if (!path) return '';
return path.split('?')[0]?.replace(/\/$/, '') || '';
}
export function isLightTabRoute(route: {
fullPath?: string;
matched?: Array<{ name?: unknown; path?: string }>;
name?: unknown;
path?: string;
}) {
if (typeof route.name === 'string' && LIGHT_MENU_NAMES.has(route.name)) {
return true;
}
if (
route.matched?.some(
(item) =>
typeof item.name === 'string' && LIGHT_MENU_NAMES.has(item.name),
)
) {
return true;
}
const path = normalizePath(route.fullPath || route.path);
return LIGHT_ROUTE_PATH_PREFIXES.some(
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
);
}
export function filterLightMenuTree< export function filterLightMenuTree<
T extends { children?: T[]; name?: string }, T extends { children?: T[]; name?: string },
>(routes: T[]): T[] { >(routes: T[]): T[] {
@@ -111,6 +111,13 @@ class PreferenceManager {
mergedPreference.widget, mergedPreference.widget,
); );
} }
if (overrides?.tabbar) {
mergedPreference.tabbar = merge(
{},
overrides.tabbar,
mergedPreference.tabbar,
);
}
if (overrides?.logo?.source) { if (overrides?.logo?.source) {
mergedPreference.logo.source = overrides.logo.source; mergedPreference.logo.source = overrides.logo.source;
} }
@@ -1,11 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, useSlots } from 'vue'; import { computed, useSlots } from 'vue';
import { preferences } from '@vben/preferences'; import { useRefresh } from '@vben/hooks';
import { RotateCw } from '@vben/icons';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
const ThemeToggle = defineAsyncComponent( import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
() => import('../../widgets/theme-toggle/theme-toggle.vue'),
); import {
GlobalSearch,
LanguageToggle,
PreferencesButton,
ThemeToggle,
TimezoneButton,
} from '../../widgets';
interface Props { interface Props {
/** /**
@@ -22,19 +31,55 @@ withDefaults(defineProps<Props>(), {
theme: 'light', theme: 'light',
}); });
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const REFERENCE_VALUE = 50; const REFERENCE_VALUE = 50;
const accessStore = useAccessStore();
const slots = useSlots(); const slots = useSlots();
const { refresh } = useRefresh();
const { globalSearchShortcutKey, preferencesButtonPosition } =
usePreferences();
const rightSlots = computed(() => { const rightSlots = computed(() => {
const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }]; const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }];
if (preferences.widget.globalSearch) {
list.push({
index: REFERENCE_VALUE,
name: 'global-search',
});
}
if (preferencesButtonPosition.value.header) {
list.push({
index: REFERENCE_VALUE + 10,
name: 'preferences',
});
}
if (preferences.widget.themeToggle) { if (preferences.widget.themeToggle) {
list.push({ list.push({
index: REFERENCE_VALUE + 20, index: REFERENCE_VALUE + 20,
name: 'theme-toggle', name: 'theme-toggle',
}); });
} }
if (preferences.widget.languageToggle) {
list.push({
index: REFERENCE_VALUE + 30,
name: 'language-toggle',
});
}
if (preferences.widget.timezone) {
list.push({
index: REFERENCE_VALUE + 40,
name: 'timezone',
});
}
if (preferences.widget.fullscreen) {
list.push({
index: REFERENCE_VALUE + 50,
name: 'fullscreen',
});
}
if (preferences.widget.notification) { if (preferences.widget.notification) {
list.push({ list.push({
index: REFERENCE_VALUE + 60, index: REFERENCE_VALUE + 60,
@@ -54,6 +99,13 @@ const rightSlots = computed(() => {
const leftSlots = computed(() => { const leftSlots = computed(() => {
const list: Array<{ index: number; name: string }> = []; const list: Array<{ index: number; name: string }> = [];
if (preferences.widget.refresh) {
list.push({
index: 0,
name: 'refresh',
});
}
Object.keys(slots).forEach((key) => { Object.keys(slots).forEach((key) => {
const name = key.split('-'); const name = key.split('-');
if (key.startsWith('header-left')) { if (key.startsWith('header-left')) {
@@ -63,6 +115,9 @@ const leftSlots = computed(() => {
return list.sort((a, b) => a.index - b.index); return list.sort((a, b) => a.index - b.index);
}); });
function clearPreferencesAndLogout() {
emit('clearPreferencesAndLogout');
}
</script> </script>
<template> <template>
@@ -71,6 +126,11 @@ const leftSlots = computed(() => {
:key="slot.name" :key="slot.name"
> >
<slot :name="slot.name"> <slot :name="slot.name">
<template v-if="slot.name === 'refresh'">
<VbenIconButton class="my-0 mr-1 rounded-md" @click="refresh">
<RotateCw class="size-4" />
</VbenIconButton>
</template>
</slot> </slot>
</template> </template>
<div class="flex-center hidden lg:block"> <div class="flex-center hidden lg:block">
@@ -91,9 +151,31 @@ const leftSlots = computed(() => {
<div class="flex h-full min-w-0 flex-shrink-0 items-center"> <div class="flex h-full min-w-0 flex-shrink-0 items-center">
<template v-for="slot in rightSlots" :key="slot.name"> <template v-for="slot in rightSlots" :key="slot.name">
<slot :name="slot.name"> <slot :name="slot.name">
<template v-if="slot.name === 'theme-toggle'"> <template v-if="slot.name === 'global-search'">
<GlobalSearch
:enable-shortcut-key="globalSearchShortcutKey"
:menus="accessStore.accessMenus"
class="mr-1 sm:mr-4"
/>
</template>
<template v-else-if="slot.name === 'preferences'">
<PreferencesButton
class="mr-1"
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
</template>
<template v-else-if="slot.name === 'theme-toggle'">
<ThemeToggle class="mr-1 mt-[2px]" /> <ThemeToggle class="mr-1 mt-[2px]" />
</template> </template>
<template v-else-if="slot.name === 'language-toggle'">
<LanguageToggle class="mr-1" />
</template>
<template v-else-if="slot.name === 'fullscreen'">
<VbenFullScreen class="mr-1" />
</template>
<template v-else-if="slot.name === 'timezone'">
<TimezoneButton class="mr-1 mt-[2px]" />
</template>
</slot> </slot>
</template> </template>
</div> </div>
@@ -14,12 +14,13 @@ import {
updatePreferences, updatePreferences,
usePreferences, usePreferences,
} from '@vben/preferences'; } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { cloneDeep, mapTree } from '@vben/utils'; import { cloneDeep, mapTree } from '@vben/utils';
import { VbenAdminLayout } from '@vben-core/layout-ui'; import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui'; import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import Breadcrumb from '../widgets/breadcrumb.vue'; import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
import { LayoutContent, LayoutContentSpinner } from './content'; import { LayoutContent, LayoutContentSpinner } from './content';
import { Copyright } from './copyright'; import { Copyright } from './copyright';
import { LayoutFooter } from './footer'; import { LayoutFooter } from './footer';
@@ -35,7 +36,7 @@ import { LayoutTabbar } from './tabbar';
defineOptions({ name: 'BasicLayout' }); defineOptions({ name: 'BasicLayout' });
const emit = defineEmits<{ clickLogo: [] }>(); const emit = defineEmits<{ clearPreferencesAndLogout: []; clickLogo: [] }>();
const { const {
isDark, isDark,
@@ -46,9 +47,11 @@ const {
isHeaderMixedNav, isHeaderMixedNav,
isHeaderSidebarNav, isHeaderSidebarNav,
layout, layout,
preferencesButtonPosition,
sidebarCollapsed, sidebarCollapsed,
theme, theme,
} = usePreferences(); } = usePreferences();
const accessStore = useAccessStore();
const { refresh } = useRefresh(); const { refresh } = useRefresh();
const sidebarTheme = computed(() => { const sidebarTheme = computed(() => {
@@ -144,6 +147,10 @@ function toggleSidebar() {
}); });
} }
function clearPreferencesAndLogout() {
emit('clearPreferencesAndLogout');
}
function clickLogo() { function clickLogo() {
emit('clickLogo'); emit('clickLogo');
} }
@@ -264,7 +271,10 @@ const headerSlots = computed(() => {
</template> </template>
<!-- 头部区域 --> <!-- 头部区域 -->
<template #header> <template #header>
<LayoutHeader :theme="theme"> <LayoutHeader
:theme="theme"
@clear-preferences-and-logout="clearPreferencesAndLogout"
>
<template <template
v-if="!showHeaderNav && preferences.breadcrumb.enable" v-if="!showHeaderNav && preferences.breadcrumb.enable"
#breadcrumb #breadcrumb
@@ -382,6 +392,21 @@ const headerSlots = computed(() => {
<template #extra> <template #extra>
<slot name="extra"></slot> <slot name="extra"></slot>
<CheckUpdates
v-if="preferences.app.enableCheckUpdates"
:check-updates-interval="preferences.app.checkUpdatesInterval"
/>
<Transition v-if="preferences.widget.lockScreen" name="slide-up">
<slot v-if="accessStore.isLockScreen" name="lock-screen"></slot>
</Transition>
<template v-if="preferencesButtonPosition.fixed">
<Preferences
class="z-100 fixed bottom-20 right-0"
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
</template>
<VbenBackTop /> <VbenBackTop />
</template> </template>
</VbenAdminLayout> </VbenAdminLayout>
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben/hooks'; import { useContentMaximize, useTabs } from '@vben/hooks';
@@ -43,7 +43,7 @@ const menus = computed(() => {
// 刷新后如果不保持tab状态,关闭其他tab // 刷新后如果不保持tab状态,关闭其他tab
if (!preferences.tabbar.persist) { if (!preferences.tabbar.persist) {
tabbarStore.closeOtherTabs(route); nextTick(() => tabbarStore.closeOtherTabs(route));
} }
</script> </script>