feat: 顶栏与用户下拉框按钮样式统一与 tooltip 修复

- 修复 VbenTooltip 的 :content prop 类型错误,改用默认插槽
- 修复 VbenIconButton 的 onClick prop 支持数组合并
- VbenFullScreen 新增 tooltip prop,支持直接设置提示
- 统一顶栏与用户下拉框所有按钮样式,使用 VbenIconButton 包装
- 移除 i18n 中多余的"启用"字样(全局搜索/语言切换)
- 修复 VbenPopover PopoverTrigger 嵌套 button 导致 tooltip 不显示

Co-Authored-By: AtomCode (GLM-5.2) <noreply@atomgit.com>
This commit is contained in:
金毛88
2026-07-21 22:36:42 +08:00
18 changed files with 614 additions and 224 deletions

View File

@@ -8,7 +8,11 @@ import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { useVbenModal } from '@vben-core/popup-ui';
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
import {
VbenFullScreen,
VbenIconButton,
VbenTooltip,
} from '@vben-core/shadcn-ui';
import { useMagicKeys, whenever } from '@vueuse/core';
@@ -16,6 +20,7 @@ import {
GlobalSearch,
LanguageToggle,
LockScreenModal,
Notification,
PreferencesButton,
ThemeToggle,
TimezoneButton,
@@ -121,94 +126,77 @@ type SlotItem = { index: number; name: string };
const rightSlots = computed(() => {
const list: Array<SlotItem> = [];
// 全局搜索
if (
preferences.widget.globalSearch &&
preferences.widget.globalSearchButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE,
name: 'global-search',
});
}
// 偏好设置快捷功能
if (preferencesButtonPosition.value.header) {
list.push({
index: REFERENCE_VALUE + 10,
name: 'preferences',
});
}
// 主题、语言、时区等子功能由各自的 widget 开关独立控制,
// 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时)
if (
preferences.widget.themeToggle &&
preferences.widget.themeToggleButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 20,
name: 'theme-toggle',
});
}
if (
preferences.widget.languageToggle &&
preferences.widget.languageToggleButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 30,
name: 'language-toggle',
});
}
if (
preferences.widget.timezone &&
preferences.widget.timezoneButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 40,
name: 'timezone',
});
}
// 全屏
if (
preferences.widget.fullscreen &&
preferences.widget.fullscreenButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 50,
name: 'fullscreen',
});
}
// 消息通知
if (
preferences.widget.notification &&
preferences.widget.notificationButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 60,
name: 'notification',
});
}
// 锁定屏幕(仅在 header 位置显示)
if (
preferences.widget.lockScreen &&
preferences.widget.lockScreenButtonPosition === 'header'
) {
list.push({
index: REFERENCE_VALUE + 70,
name: 'lock-screen-btn',
});
}
// 退出登录(仅在 header 位置显示)
if (preferences.widget.logoutButtonPosition === 'header') {
list.push({
index: REFERENCE_VALUE + 80,
name: 'logout-btn',
});
// 按 widget.order 顺序遍历,检查每个部件是否应在 header 中显示
const widgetChecks: Record<string, { slotName: string; visible: boolean }> = {
globalSearch: {
visible:
preferences.widget.globalSearch &&
preferences.widget.globalSearchButtonPosition === 'header',
slotName: 'global-search',
},
preferences: {
visible: preferencesButtonPosition.value.header,
slotName: 'preferences',
},
themeToggle: {
visible:
preferences.widget.themeToggle &&
preferences.widget.themeToggleButtonPosition === 'header',
slotName: 'theme-toggle',
},
languageToggle: {
visible:
preferences.widget.languageToggle &&
preferences.widget.languageToggleButtonPosition === 'header',
slotName: 'language-toggle',
},
timezone: {
visible:
preferences.widget.timezone &&
preferences.widget.timezoneButtonPosition === 'header',
slotName: 'timezone',
},
fullscreen: {
visible:
preferences.widget.fullscreen &&
preferences.widget.fullscreenButtonPosition === 'header',
slotName: 'fullscreen',
},
notification: {
visible:
preferences.widget.notification &&
preferences.widget.notificationButtonPosition === 'header',
slotName: 'notification',
},
lockScreenBtn: {
visible:
preferences.widget.lockScreen &&
preferences.widget.lockScreenButtonPosition === 'header',
slotName: 'lock-screen-btn',
},
logoutBtn: {
visible: preferences.widget.logoutButtonPosition === 'header',
slotName: 'logout-btn',
},
refresh: {
visible:
preferences.widget.refresh &&
preferences.widget.refreshButtonPosition === 'header',
slotName: 'refresh',
},
};
for (const key of preferences.widget.order) {
const check = widgetChecks[key];
if (check?.visible) {
list.push({ index: REFERENCE_VALUE + list.length, name: check.slotName });
}
}
// 用户插槽header-right-N追加在后面
Object.keys(slots).forEach((key) => {
// 适配插槽名称例如第一个插槽名header-right-1
if (key.startsWith('header-right')) {
// 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引
const slotIndex = Number(key.split('-')[2]);
const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex;
list.push({ index, name: key });
@@ -223,16 +211,6 @@ const rightSlots = computed(() => {
const leftSlots = computed(() => {
const list: Array<SlotItem> = [];
// 刷新
if (
preferences.widget.refresh &&
preferences.widget.refreshButtonPosition === 'header'
) {
list.push({
index: 0,
name: 'refresh',
});
}
Object.keys(slots).forEach((key) => {
// 适配插槽名称例如第一个插槽名header-left-1
@@ -322,19 +300,37 @@ function clearPreferencesAndLogout() {
</template>
<template v-else-if="slot.name === 'preferences'">
<PreferencesButton
class="mr-1"
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
<VbenTooltip side="bottom">
<template #trigger>
<PreferencesButton
class="mr-1"
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
</template>
{{ $t('preferences.title') }}
</VbenTooltip>
</template>
<template v-else-if="slot.name === 'theme-toggle'">
<ThemeToggle class="mt-0.5 mr-1" />
<VbenTooltip side="bottom">
<template #trigger>
<ThemeToggle class="mt-0.5 mr-1" />
</template>
{{ $t('preferences.theme.title') }}
</VbenTooltip>
</template>
<template v-else-if="slot.name === 'language-toggle'">
<LanguageToggle class="mr-1" />
<VbenTooltip side="bottom">
<template #trigger>
<LanguageToggle class="mr-1" />
</template>
{{ $t('preferences.widget.languageToggle') }}
</VbenTooltip>
</template>
<template v-else-if="slot.name === 'fullscreen'">
<VbenFullScreen class="mr-1" />
<VbenFullScreen
class="mr-1"
:tooltip="$t('preferences.widget.fullscreen')"
/>
</template>
<template v-else-if="slot.name === 'timezone'">
<TimezoneButton class="mt-0.5 mr-1" />
@@ -357,6 +353,23 @@ function clearPreferencesAndLogout() {
<LogOut class="size-4" />
</VbenIconButton>
</template>
<template v-else-if="slot.name === 'notification'">
<VbenTooltip side="bottom">
<template #trigger>
<Notification class="mr-1" />
</template>
{{ $t('preferences.widget.notification') }}
</VbenTooltip>
</template>
<template v-else-if="slot.name === 'refresh'">
<VbenIconButton
class="my-0 mr-1 rounded-md"
:tooltip="$t('preferences.widget.refresh')"
@click="refresh"
>
<RotateCw class="size-4" />
</VbenIconButton>
</template>
</slot>
</template>
</div>