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:
10
packages/@core/base/typings/src/app.d.ts
vendored
10
packages/@core/base/typings/src/app.d.ts
vendored
@@ -10,16 +10,18 @@ type LayoutType =
|
||||
type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||
|
||||
/**
|
||||
* 按钮位置
|
||||
* user-dropdown 用户的下拉弹出框中
|
||||
* fixed 固定在右侧
|
||||
* 偏好设置按钮位置
|
||||
* auto 自动(按布局上下文在 header/fixed 间切换)
|
||||
* fixed 固定在屏幕右边缘
|
||||
* header 顶栏
|
||||
* auto 自动
|
||||
* user-dropdown 用户的下拉弹出框中
|
||||
* none 不显示
|
||||
*/
|
||||
type PreferencesButtonPositionType =
|
||||
| 'auto'
|
||||
| 'fixed'
|
||||
| 'header'
|
||||
| 'none'
|
||||
| 'user-dropdown';
|
||||
|
||||
type BuiltinThemeType =
|
||||
|
||||
@@ -154,6 +154,17 @@ const defaultPreferences: Preferences = {
|
||||
themeToggleButtonPosition: 'header',
|
||||
timezone: true,
|
||||
timezoneButtonPosition: 'header',
|
||||
order: [
|
||||
'globalSearch',
|
||||
'preferences',
|
||||
'themeToggle',
|
||||
'languageToggle',
|
||||
'timezone',
|
||||
'fullscreen',
|
||||
'notification',
|
||||
'lockScreenBtn',
|
||||
'logoutBtn',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ import type {
|
||||
import { markRaw, reactive, readonly, watch } from 'vue';
|
||||
|
||||
import { StorageManager } from '@vben-core/shared/cache';
|
||||
import { isMacOs, merge } from '@vben-core/shared/utils';
|
||||
import {
|
||||
isMacOs,
|
||||
merge,
|
||||
mergeWithArrayOverride,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
@@ -133,7 +137,11 @@ class PreferenceManager {
|
||||
|
||||
// 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段
|
||||
const cachedPreferences = (await this.loadFromCache()) || {};
|
||||
const mergedPreference = merge(
|
||||
// 修复历史脏数据:早期版本用 defu(merge) 合并数组,导致 widget.order
|
||||
// 在每次 updatePreferences 时被 concat 追加,膨胀到几十/上百条重复 key。
|
||||
// 这里在加载缓存后做一次去重,保留首次出现的 key。
|
||||
this.sanitizeCachedArray(cachedPreferences, 'widget', 'order');
|
||||
const mergedPreference = mergeWithArrayOverride(
|
||||
{},
|
||||
cachedPreferences, // 用户缓存的设置优先
|
||||
this.initialPreferences, // 初始设置仅补齐缺失字段
|
||||
@@ -144,7 +152,7 @@ class PreferenceManager {
|
||||
|
||||
const cachedCustom = (await this.loadCustomFromCache()) || {};
|
||||
this.replaceCustomPreferences(
|
||||
merge(
|
||||
mergeWithArrayOverride(
|
||||
{},
|
||||
this.sanitizeCustomPreferences(cachedCustom),
|
||||
this.initialCustomPreferences,
|
||||
@@ -194,7 +202,7 @@ class PreferenceManager {
|
||||
}
|
||||
|
||||
this.replaceCustomPreferences(
|
||||
merge({}, sanitizedUpdates, markRaw(this.customState)),
|
||||
mergeWithArrayOverride({}, sanitizedUpdates, markRaw(this.customState)),
|
||||
);
|
||||
this.debouncedSave();
|
||||
};
|
||||
@@ -205,7 +213,14 @@ class PreferenceManager {
|
||||
*/
|
||||
updatePreferences = (updates: DeepPartial<Preferences>) => {
|
||||
// 深度合并更新内容和当前状态
|
||||
const mergedState = merge({}, updates, markRaw(this.state));
|
||||
// 注意:必须用 mergeWithArrayOverride 而不是 merge。
|
||||
// defu(merge) 对数组的语义是 concat 追加,会导致 widget.order 等
|
||||
// 数组字段在每次 updatePreferences 时被重复追加,指数级膨胀。
|
||||
const mergedState = mergeWithArrayOverride(
|
||||
{},
|
||||
updates,
|
||||
markRaw(this.state),
|
||||
);
|
||||
Object.assign(this.state, mergedState);
|
||||
|
||||
// 根据更新的值执行更新
|
||||
@@ -361,6 +376,28 @@ class PreferenceManager {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存中膨胀的数组字段(保留首次出现的元素)
|
||||
* 用于修复早期版本 defu 合并数组时 concat 追加导致的脏数据
|
||||
*/
|
||||
private sanitizeCachedArray(
|
||||
cached: Record<string, any>,
|
||||
group: string,
|
||||
field: string,
|
||||
) {
|
||||
const node = cached?.[group]?.[field];
|
||||
if (!Array.isArray(node) || node.length <= 1) return;
|
||||
const seen = new Set<unknown>();
|
||||
const deduped = node.filter((item) => {
|
||||
if (seen.has(item)) return false;
|
||||
seen.add(item);
|
||||
return true;
|
||||
});
|
||||
if (deduped.length !== node.length) {
|
||||
cached[group][field] = deduped;
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeCustomPreferences(
|
||||
updates: DeepPartial<CustomPreferencesRecord>,
|
||||
) {
|
||||
|
||||
@@ -385,6 +385,8 @@ interface WidgetPreferences {
|
||||
notification: boolean;
|
||||
/** 通知按钮位置 */
|
||||
notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 小部件排序 */
|
||||
order: readonly string[];
|
||||
/** 显示刷新按钮 */
|
||||
refresh: boolean;
|
||||
/** 刷新按钮位置 */
|
||||
|
||||
@@ -12,7 +12,7 @@ import VbenButton from './button.vue';
|
||||
interface Props extends VbenButtonProps {
|
||||
class?: any;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (() => void)[] | (() => void);
|
||||
tooltip?: string;
|
||||
tooltipDelayDuration?: number;
|
||||
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
||||
@@ -30,6 +30,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const slots = useSlots();
|
||||
|
||||
const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
||||
|
||||
function handleClick() {
|
||||
if (Array.isArray(props.onClick)) {
|
||||
for (const fn of props.onClick) {
|
||||
fn?.();
|
||||
}
|
||||
} else {
|
||||
props.onClick?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,7 +49,7 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
||||
:disabled="disabled"
|
||||
:variant="variant"
|
||||
size="icon"
|
||||
@click="onClick"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</VbenButton>
|
||||
@@ -55,7 +65,7 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
||||
:disabled="disabled"
|
||||
:variant="variant"
|
||||
size="icon"
|
||||
@click="onClick"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</VbenButton>
|
||||
|
||||
@@ -7,6 +7,15 @@ import { VbenIconButton } from '../button';
|
||||
|
||||
defineOptions({ name: 'FullScreen' });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
tooltip?: string;
|
||||
}>(),
|
||||
{
|
||||
tooltip: '',
|
||||
},
|
||||
);
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
// 重新检查全屏状态
|
||||
@@ -22,6 +31,7 @@ isFullscreen.value = !!(
|
||||
</script>
|
||||
<template>
|
||||
<VbenIconButton
|
||||
:tooltip="tooltip || undefined"
|
||||
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
||||
@click="toggle"
|
||||
>
|
||||
|
||||
@@ -45,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-bind="forwarded">
|
||||
<PopoverTrigger :class="triggerClass">
|
||||
<PopoverTrigger as="span" :class="triggerClass">
|
||||
<slot name="trigger"></slot>
|
||||
|
||||
<PopoverContent
|
||||
|
||||
Reference in New Issue
Block a user