feat: 偏好设置小部件支持位置选择与拖拽排序
- 顶栏各小部件按钮支持选择位置(header/user-dropdown/none),偏好设置按钮额外保留 auto/fixed 智能兜底,移动端/全屏内容模式下自动切 fixed 浮动保命,避免偏好入口死锁 - draggable-list.vue 的 listRef 未在模板绑定,Sortable 永不初始化导致小部件无法拖拽;加 ref 绑定与 data-key 让 onEnd 从 DOM 读真实新顺序 - preferences.ts 的 updatePreferences 用 defu(merge) 合并数组,对 widget.order 是 concat 语义,每次拖拽触发更新都会把当前 order 追加一次,localStorage 里膨胀到上百条;改用 mergeWithArrayOverride 整体替换,加载缓存时 sanitizeCachedArray 去重已存在的脏数据 - preferences 按钮之前在 widget.order 里因 labelMap 未登记显示成裸字符串,且 widgetChecks 缺 refresh 导致刷新按钮不按 order 渲染;统一 preferences 与其它 widget 的拖拽排序与位置 select,refresh 补进 rightSlots.widgetChecks 并补模板渲染 - 补 sortablejs 依赖、WidgetPreferences.order 类型字段、preferences-drawer.vue 的 widgetOrder 绑定
This commit is contained in:
@@ -231,6 +231,7 @@ watch(
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
|
||||
@@ -231,6 +231,7 @@ watch(
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
|
||||
@@ -231,6 +231,7 @@ watch(
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
|
||||
@@ -231,6 +231,7 @@ watch(
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
|
||||
@@ -231,6 +231,7 @@ watch(
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@changesets/cli": "catalog:",
|
||||
"@tsdown/css": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/sortablejs": "catalog:",
|
||||
"@vben/commitlint-config": "workspace:*",
|
||||
"@vben/eslint-config": "workspace:*",
|
||||
"@vben/oxfmt-config": "workspace:*",
|
||||
@@ -106,5 +107,8 @@
|
||||
"node": "^22.18.0 || ^24.0.0",
|
||||
"pnpm": ">=11.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.7.0"
|
||||
"packageManager": "pnpm@11.7.0",
|
||||
"dependencies": {
|
||||
"sortablejs": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
@@ -137,14 +137,34 @@ const defaultPreferences: Preferences = {
|
||||
},
|
||||
widget: {
|
||||
fullscreen: true,
|
||||
fullscreenButtonPosition: 'header',
|
||||
globalSearch: true,
|
||||
globalSearchButtonPosition: 'header',
|
||||
languageToggle: true,
|
||||
languageToggleButtonPosition: 'header',
|
||||
lockScreen: true,
|
||||
lockScreenButtonPosition: 'header',
|
||||
logoutButtonPosition: 'header',
|
||||
notification: true,
|
||||
notificationButtonPosition: 'header',
|
||||
refresh: true,
|
||||
refreshButtonPosition: 'header',
|
||||
sidebarToggle: true,
|
||||
themeToggle: true,
|
||||
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>,
|
||||
) {
|
||||
|
||||
@@ -365,22 +365,42 @@ interface TransitionPreferences {
|
||||
interface WidgetPreferences {
|
||||
/** 是否启用全屏部件 */
|
||||
fullscreen: boolean;
|
||||
/** 全屏按钮位置 */
|
||||
fullscreenButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否启用全局搜索部件 */
|
||||
globalSearch: boolean;
|
||||
/** 全局搜索按钮位置 */
|
||||
globalSearchButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否启用语言切换部件 */
|
||||
languageToggle: boolean;
|
||||
/** 语言切换按钮位置 */
|
||||
languageToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否开启锁屏功能 */
|
||||
lockScreen: boolean;
|
||||
/** 锁屏按钮位置 */
|
||||
lockScreenButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 退出登录按钮位置 */
|
||||
logoutButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示通知部件 */
|
||||
notification: boolean;
|
||||
/** 通知按钮位置 */
|
||||
notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 显示刷新按钮 */
|
||||
refresh: boolean;
|
||||
/** 刷新按钮位置 */
|
||||
refreshButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示侧边栏显示/隐藏部件 */
|
||||
sidebarToggle: boolean;
|
||||
/** 是否显示主题切换部件 */
|
||||
themeToggle: boolean;
|
||||
/** 主题切换按钮位置 */
|
||||
themeToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示时区部件 */
|
||||
timezone: boolean;
|
||||
/** 时区按钮位置 */
|
||||
timezoneButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 小部件排序 */
|
||||
order: readonly string[];
|
||||
}
|
||||
|
||||
interface Preferences {
|
||||
|
||||
@@ -121,71 +121,77 @@ type SlotItem = { index: number; name: string };
|
||||
|
||||
const rightSlots = computed(() => {
|
||||
const list: Array<SlotItem> = [];
|
||||
// 全局搜索
|
||||
if (preferences.widget.globalSearch) {
|
||||
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) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 20,
|
||||
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) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 60,
|
||||
name: 'notification',
|
||||
});
|
||||
}
|
||||
// 锁定屏幕
|
||||
if (preferences.widget.lockScreen) {
|
||||
list.push({
|
||||
index: REFERENCE_VALUE + 70,
|
||||
name: 'lock-screen-btn',
|
||||
});
|
||||
}
|
||||
// 退出登录
|
||||
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 });
|
||||
@@ -200,13 +206,6 @@ const rightSlots = computed(() => {
|
||||
|
||||
const leftSlots = computed(() => {
|
||||
const list: Array<SlotItem> = [];
|
||||
// 刷新
|
||||
if (preferences.widget.refresh) {
|
||||
list.push({
|
||||
index: 0,
|
||||
name: 'refresh',
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(slots).forEach((key) => {
|
||||
// 适配插槽名称,例如第一个插槽名:header-left-1
|
||||
@@ -331,6 +330,15 @@ function clearPreferencesAndLogout() {
|
||||
<LogOut class="size-4" />
|
||||
</VbenIconButton>
|
||||
</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>
|
||||
|
||||
@@ -369,9 +369,6 @@ const headerSlots = computed(() => {
|
||||
<template #notification>
|
||||
<slot name="notification"></slot>
|
||||
</template>
|
||||
<template #timezone>
|
||||
<slot name="timezone"></slot>
|
||||
</template>
|
||||
<template v-for="item in headerSlots" #[item]>
|
||||
<slot :name="item"></slot>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectOption } from '@vben/types';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { GripVertical } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
interface Item {
|
||||
key: string;
|
||||
label: string;
|
||||
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown';
|
||||
/** 每项可选的 position 选项;不传则用 props.positionItems */
|
||||
positionItems?: SelectOption[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
items: Item[];
|
||||
positionItems: SelectOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateOrder: [keys: string[]];
|
||||
updatePosition: [
|
||||
key: string,
|
||||
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||
];
|
||||
}>();
|
||||
|
||||
const listRef = ref<HTMLElement>();
|
||||
let sortableInstance: null | Sortable = null;
|
||||
|
||||
// 划分规则:
|
||||
// - hidden:position === 'none'(不显示)
|
||||
// - sortable:其它(含 auto/fixed,偏好按钮的智能模式视觉归入顶栏组参与排序,
|
||||
// 避免 preferences 从 widget.order 丢失)
|
||||
const sortableList = computed(() =>
|
||||
props.items.filter((item) => item.position !== 'none'),
|
||||
);
|
||||
|
||||
const hiddenList = computed(() =>
|
||||
props.items.filter((item) => item.position === 'none'),
|
||||
);
|
||||
function initSortable() {
|
||||
if (!listRef.value) return;
|
||||
sortableInstance?.destroy();
|
||||
sortableInstance = Sortable.create(listRef.value, {
|
||||
animation: 200,
|
||||
handle: '.drag-handle',
|
||||
onEnd() {
|
||||
// Sortable 已经改了 DOM,但 sortableList computed 还是旧顺序。
|
||||
// 直接从 DOM 读 children 的 data-key 拿新顺序,再追加 hidden 部分。
|
||||
const newOrder = [...listRef.value!.children]
|
||||
.map((el) => (el as HTMLElement).dataset.key)
|
||||
.filter(Boolean) as string[];
|
||||
emit('updateOrder', [...newOrder, ...hiddenList.value.map((i) => i.key)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(initSortable);
|
||||
|
||||
function setPosition(key: string, event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value as
|
||||
| 'auto'
|
||||
| 'fixed'
|
||||
| 'header'
|
||||
| 'none'
|
||||
| 'user-dropdown';
|
||||
emit('updatePosition', key, value);
|
||||
nextTick(() => {
|
||||
emit(
|
||||
'updateOrder',
|
||||
[...sortableList.value, ...hiddenList.value].map((i) => i.key),
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<div ref="listRef" class="space-y-1">
|
||||
<div
|
||||
v-for="item in sortableList"
|
||||
:key="item.key"
|
||||
:data-key="item.key"
|
||||
class="bg-accent flex items-center gap-2 rounded-md px-2 py-1.5"
|
||||
>
|
||||
<GripVertical
|
||||
class="drag-handle size-4 shrink-0 cursor-grab text-muted-foreground active:cursor-grabbing"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-sm">{{ item.label }}</span>
|
||||
<select
|
||||
:value="item.position"
|
||||
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||
@change="(e) => setPosition(item.key, e)"
|
||||
>
|
||||
<option
|
||||
v-for="opt in item.positionItems ?? positionItems"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hiddenList.length > 0" class="pt-2">
|
||||
<div class="text-muted-foreground mb-1 text-xs font-medium">
|
||||
{{ $t('preferences.widget.hidden') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in hiddenList"
|
||||
:key="item.key"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1"
|
||||
>
|
||||
<span
|
||||
class="text-muted-foreground min-w-0 flex-1 truncate text-sm line-through decoration-dotted"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<select
|
||||
:value="item.position"
|
||||
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||
@change="(e) => setPosition(item.key, e)"
|
||||
>
|
||||
<option
|
||||
v-for="opt in item.positionItems ?? positionItems"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,31 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectOption } from '@vben/types';
|
||||
import type { PreferencesButtonPositionType, SelectOption } from '@vben/types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SelectItem from '../select-item.vue';
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
import DraggableList from '../draggable-list.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceInterfaceControl',
|
||||
});
|
||||
|
||||
const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
|
||||
const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
|
||||
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
|
||||
const widgetNotification = defineModel<boolean>('widgetNotification');
|
||||
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
|
||||
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
|
||||
const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
|
||||
const appPreferencesButtonPosition = defineModel<string>(
|
||||
const widgetOrder = defineModel<string[]>('widgetOrder', { required: true });
|
||||
|
||||
const widgetGlobalSearchButtonPosition = defineModel<string>(
|
||||
'widgetGlobalSearchButtonPosition',
|
||||
);
|
||||
const widgetFullscreenButtonPosition = defineModel<string>(
|
||||
'widgetFullscreenButtonPosition',
|
||||
);
|
||||
const widgetLanguageToggleButtonPosition = defineModel<string>(
|
||||
'widgetLanguageToggleButtonPosition',
|
||||
);
|
||||
const widgetNotificationButtonPosition = defineModel<string>(
|
||||
'widgetNotificationButtonPosition',
|
||||
);
|
||||
const widgetThemeToggleButtonPosition = defineModel<string>(
|
||||
'widgetThemeToggleButtonPosition',
|
||||
);
|
||||
const widgetLockScreenButtonPosition = defineModel<string>(
|
||||
'widgetLockScreenButtonPosition',
|
||||
);
|
||||
const widgetLogoutButtonPosition = defineModel<string>(
|
||||
'widgetLogoutButtonPosition',
|
||||
);
|
||||
const widgetRefreshButtonPosition = defineModel<string>(
|
||||
'widgetRefreshButtonPosition',
|
||||
);
|
||||
const widgetTimezoneButtonPosition = defineModel<string>(
|
||||
'widgetTimezoneButtonPosition',
|
||||
);
|
||||
const appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(
|
||||
'appPreferencesButtonPosition',
|
||||
);
|
||||
const widgetRefresh = defineModel<boolean>('widgetRefresh');
|
||||
const widgetTimezone = defineModel<boolean>('widgetTimezone');
|
||||
|
||||
const positionItems = computed((): SelectOption[] => [
|
||||
const buttonPositionItems = computed((): SelectOption[] => [
|
||||
{
|
||||
label: $t('preferences.widget.header'),
|
||||
value: 'header',
|
||||
},
|
||||
{
|
||||
label: $t('preferences.widget.userDropdown'),
|
||||
value: 'user-dropdown',
|
||||
},
|
||||
{
|
||||
label: $t('common.notShow'),
|
||||
value: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* preferences 按钮独享的位置选项:
|
||||
* 保留 auto/fixed 让 use-preferences.ts 的智能 fallback 生效
|
||||
* (移动端/全屏内容模式下自动切换 header/fixed,避免偏好入口死锁)
|
||||
*/
|
||||
const preferencesPositionItems = computed((): SelectOption[] => [
|
||||
{
|
||||
label: $t('preferences.position.auto'),
|
||||
value: 'auto',
|
||||
@@ -42,38 +81,99 @@ const positionItems = computed((): SelectOption[] => [
|
||||
label: $t('preferences.position.userDropdown'),
|
||||
value: 'user-dropdown',
|
||||
},
|
||||
{
|
||||
label: $t('common.notShow'),
|
||||
value: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const positionMap: Record<string, string> = {
|
||||
globalSearch: 'widgetGlobalSearchButtonPosition',
|
||||
preferences: 'appPreferencesButtonPosition',
|
||||
themeToggle: 'widgetThemeToggleButtonPosition',
|
||||
languageToggle: 'widgetLanguageToggleButtonPosition',
|
||||
timezone: 'widgetTimezoneButtonPosition',
|
||||
fullscreen: 'widgetFullscreenButtonPosition',
|
||||
notification: 'widgetNotificationButtonPosition',
|
||||
lockScreenBtn: 'widgetLockScreenButtonPosition',
|
||||
logoutBtn: 'widgetLogoutButtonPosition',
|
||||
refresh: 'widgetRefreshButtonPosition',
|
||||
};
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
globalSearch: 'preferences.widget.globalSearch',
|
||||
preferences: 'preferences.title',
|
||||
themeToggle: 'preferences.widget.themeToggle',
|
||||
languageToggle: 'preferences.widget.languageToggle',
|
||||
timezone: 'preferences.widget.timezone',
|
||||
fullscreen: 'preferences.widget.fullscreen',
|
||||
notification: 'preferences.widget.notification',
|
||||
lockScreenBtn: 'ui.widgets.lockScreen.title',
|
||||
logoutBtn: 'common.logout',
|
||||
refresh: 'preferences.widget.refresh',
|
||||
};
|
||||
|
||||
const draggableItems = computed(() =>
|
||||
(widgetOrder.value ?? []).map((key) => ({
|
||||
key,
|
||||
label: $t(labelMap[key] ?? key),
|
||||
position: getPosition(key),
|
||||
positionItems:
|
||||
key === 'preferences' ? preferencesPositionItems.value : undefined,
|
||||
})),
|
||||
);
|
||||
|
||||
function getPosition(
|
||||
key: string,
|
||||
): 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown' {
|
||||
const modelName = positionMap[key];
|
||||
if (!modelName) return 'none';
|
||||
const modelMap: Record<string, any> = {
|
||||
widgetGlobalSearchButtonPosition,
|
||||
widgetFullscreenButtonPosition,
|
||||
widgetLanguageToggleButtonPosition,
|
||||
widgetNotificationButtonPosition,
|
||||
widgetThemeToggleButtonPosition,
|
||||
widgetLockScreenButtonPosition,
|
||||
widgetLogoutButtonPosition,
|
||||
widgetRefreshButtonPosition,
|
||||
widgetTimezoneButtonPosition,
|
||||
appPreferencesButtonPosition,
|
||||
};
|
||||
return modelMap[modelName]?.value ?? 'none';
|
||||
}
|
||||
|
||||
function handleUpdateOrder(keys: string[]) {
|
||||
widgetOrder.value = keys;
|
||||
}
|
||||
|
||||
function handleUpdatePosition(
|
||||
key: string,
|
||||
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||
) {
|
||||
const modelName = positionMap[key];
|
||||
if (!modelName) return;
|
||||
const modelMap: Record<string, any> = {
|
||||
widgetGlobalSearchButtonPosition,
|
||||
widgetFullscreenButtonPosition,
|
||||
widgetLanguageToggleButtonPosition,
|
||||
widgetNotificationButtonPosition,
|
||||
widgetThemeToggleButtonPosition,
|
||||
widgetLockScreenButtonPosition,
|
||||
widgetLogoutButtonPosition,
|
||||
widgetRefreshButtonPosition,
|
||||
widgetTimezoneButtonPosition,
|
||||
appPreferencesButtonPosition,
|
||||
};
|
||||
modelMap[modelName].value = position;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="widgetGlobalSearch">
|
||||
{{ $t('preferences.widget.globalSearch') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetThemeToggle">
|
||||
{{ $t('preferences.widget.themeToggle') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetLanguageToggle">
|
||||
{{ $t('preferences.widget.languageToggle') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetFullscreen">
|
||||
{{ $t('preferences.widget.fullscreen') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetNotification">
|
||||
{{ $t('preferences.widget.notification') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetLockScreen">
|
||||
{{ $t('preferences.widget.lockScreen') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetSidebarToggle">
|
||||
{{ $t('preferences.widget.sidebarToggle') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetRefresh">
|
||||
{{ $t('preferences.widget.refresh') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="widgetTimezone">
|
||||
{{ $t('preferences.widget.timezone') }}
|
||||
</SwitchItem>
|
||||
<SelectItem v-model="appPreferencesButtonPosition" :items="positionItems">
|
||||
{{ $t('preferences.position.title') }}
|
||||
</SelectItem>
|
||||
<DraggableList
|
||||
:items="draggableItems"
|
||||
:position-items="buttonPositionItems"
|
||||
@update-order="handleUpdateOrder"
|
||||
@update-position="handleUpdatePosition"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -174,15 +174,34 @@ const shortcutKeysGlobalLockScreen = defineModel<boolean>(
|
||||
'shortcutKeysGlobalLockScreen',
|
||||
);
|
||||
|
||||
const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
|
||||
const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
|
||||
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
|
||||
const widgetNotification = defineModel<boolean>('widgetNotification');
|
||||
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
|
||||
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
|
||||
const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
|
||||
const widgetRefresh = defineModel<boolean>('widgetRefresh');
|
||||
const widgetTimezone = defineModel<boolean>('widgetTimezone');
|
||||
const widgetGlobalSearchButtonPosition = defineModel<string>(
|
||||
'widgetGlobalSearchButtonPosition',
|
||||
);
|
||||
const widgetFullscreenButtonPosition = defineModel<string>(
|
||||
'widgetFullscreenButtonPosition',
|
||||
);
|
||||
const widgetLanguageToggleButtonPosition = defineModel<string>(
|
||||
'widgetLanguageToggleButtonPosition',
|
||||
);
|
||||
const widgetNotificationButtonPosition = defineModel<string>(
|
||||
'widgetNotificationButtonPosition',
|
||||
);
|
||||
const widgetThemeToggleButtonPosition = defineModel<string>(
|
||||
'widgetThemeToggleButtonPosition',
|
||||
);
|
||||
const widgetLockScreenButtonPosition = defineModel<string>(
|
||||
'widgetLockScreenButtonPosition',
|
||||
);
|
||||
const widgetLogoutButtonPosition = defineModel<string>(
|
||||
'widgetLogoutButtonPosition',
|
||||
);
|
||||
const widgetOrder = defineModel<string[]>('widgetOrder', { required: true });
|
||||
const widgetRefreshButtonPosition = defineModel<string>(
|
||||
'widgetRefreshButtonPosition',
|
||||
);
|
||||
const widgetTimezoneButtonPosition = defineModel<string>(
|
||||
'widgetTimezoneButtonPosition',
|
||||
);
|
||||
|
||||
const {
|
||||
customPreferences,
|
||||
@@ -483,15 +502,34 @@ function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) {
|
||||
v-model:app-preferences-button-position="
|
||||
appPreferencesButtonPosition
|
||||
"
|
||||
v-model:widget-fullscreen="widgetFullscreen"
|
||||
v-model:widget-global-search="widgetGlobalSearch"
|
||||
v-model:widget-language-toggle="widgetLanguageToggle"
|
||||
v-model:widget-lock-screen="widgetLockScreen"
|
||||
v-model:widget-notification="widgetNotification"
|
||||
v-model:widget-refresh="widgetRefresh"
|
||||
v-model:widget-sidebar-toggle="widgetSidebarToggle"
|
||||
v-model:widget-theme-toggle="widgetThemeToggle"
|
||||
v-model:widget-timezone="widgetTimezone"
|
||||
v-model:widget-fullscreen-button-position="
|
||||
widgetFullscreenButtonPosition
|
||||
"
|
||||
v-model:widget-global-search-button-position="
|
||||
widgetGlobalSearchButtonPosition
|
||||
"
|
||||
v-model:widget-language-toggle-button-position="
|
||||
widgetLanguageToggleButtonPosition
|
||||
"
|
||||
v-model:widget-lock-screen-button-position="
|
||||
widgetLockScreenButtonPosition
|
||||
"
|
||||
v-model:widget-logout-button-position="
|
||||
widgetLogoutButtonPosition
|
||||
"
|
||||
v-model:widget-order="widgetOrder"
|
||||
v-model:widget-notification-button-position="
|
||||
widgetNotificationButtonPosition
|
||||
"
|
||||
v-model:widget-refresh-button-position="
|
||||
widgetRefreshButtonPosition
|
||||
"
|
||||
v-model:widget-theme-toggle-button-position="
|
||||
widgetThemeToggleButtonPosition
|
||||
"
|
||||
v-model:widget-timezone-button-position="
|
||||
widgetTimezoneButtonPosition
|
||||
"
|
||||
/>
|
||||
</Block>
|
||||
<Block :title="$t('preferences.footer.title')">
|
||||
|
||||
@@ -3,13 +3,27 @@ import type { Component } from 'vue';
|
||||
|
||||
import type { AnyFunction } from '@vben/types';
|
||||
|
||||
import { useTemplateRef, watch } from 'vue';
|
||||
import { computed, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { useHoverToggle } from '@vben/hooks';
|
||||
import { Settings } from '@vben/icons';
|
||||
import {
|
||||
Bell,
|
||||
Fullscreen,
|
||||
Languages,
|
||||
LockKeyhole,
|
||||
LogOut,
|
||||
RotateCw,
|
||||
Search,
|
||||
Settings,
|
||||
Sun,
|
||||
SunMoon,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { isWindowsOs } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben-core/popup-ui';
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
@@ -17,11 +31,15 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
VbenAvatar,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||
|
||||
import { LockScreenModal } from '../lock-screen';
|
||||
import { Preferences } from '../preferences';
|
||||
|
||||
interface Props {
|
||||
@@ -70,9 +88,22 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
hoverDelay: 500,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||
const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
|
||||
|
||||
const { preferencesButtonPosition } = usePreferences();
|
||||
const {
|
||||
globalLockScreenShortcutKey,
|
||||
globalLogoutShortcutKey,
|
||||
preferencesButtonPosition,
|
||||
} = usePreferences();
|
||||
const accessStore = useAccessStore();
|
||||
const [LockModal, lockModalApi] = useVbenModal({
|
||||
connectedComponent: LockScreenModal,
|
||||
});
|
||||
const [LogoutModal, logoutModalApi] = useVbenModal({
|
||||
onConfirm() {
|
||||
handleSubmitLogout();
|
||||
},
|
||||
});
|
||||
|
||||
const refTrigger = useTemplateRef('refTrigger');
|
||||
const refContent = useTemplateRef('refContent');
|
||||
@@ -96,13 +127,157 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const showLockInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.lockScreen &&
|
||||
preferences.widget.lockScreenButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showLogoutInDropdown = computed(
|
||||
() => preferences.widget.logoutButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showGlobalSearchInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.globalSearch &&
|
||||
preferences.widget.globalSearchButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showThemeToggleInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.themeToggle &&
|
||||
preferences.widget.themeToggleButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showLanguageToggleInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.languageToggle &&
|
||||
preferences.widget.languageToggleButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showTimezoneInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.timezone &&
|
||||
preferences.widget.timezoneButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showFullscreenInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.fullscreen &&
|
||||
preferences.widget.fullscreenButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showNotificationInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.notification &&
|
||||
preferences.widget.notificationButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const showRefreshInDropdown = computed(
|
||||
() =>
|
||||
preferences.widget.refresh &&
|
||||
preferences.widget.refreshButtonPosition === 'user-dropdown',
|
||||
);
|
||||
|
||||
const hasAnyInDropdown = computed(
|
||||
() =>
|
||||
showLockInDropdown.value ||
|
||||
showLogoutInDropdown.value ||
|
||||
showGlobalSearchInDropdown.value ||
|
||||
showThemeToggleInDropdown.value ||
|
||||
showLanguageToggleInDropdown.value ||
|
||||
showTimezoneInDropdown.value ||
|
||||
showFullscreenInDropdown.value ||
|
||||
showNotificationInDropdown.value ||
|
||||
showRefreshInDropdown.value,
|
||||
);
|
||||
|
||||
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
||||
|
||||
const enableLogoutShortcutKey = computed(() => {
|
||||
return showLogoutInDropdown.value && globalLogoutShortcutKey.value;
|
||||
});
|
||||
|
||||
const enableLockScreenShortcutKey = computed(() => {
|
||||
return showLockInDropdown.value && globalLockScreenShortcutKey.value;
|
||||
});
|
||||
|
||||
const enableShortcutKey = computed(() => {
|
||||
return (
|
||||
(showLockInDropdown.value || showLogoutInDropdown.value) &&
|
||||
preferences.shortcutKeys.enable
|
||||
);
|
||||
});
|
||||
|
||||
function handleOpenLock() {
|
||||
lockModalApi.open();
|
||||
}
|
||||
|
||||
function handleSubmitLock(lockScreenPassword: string) {
|
||||
lockModalApi.close();
|
||||
accessStore.lockScreen(lockScreenPassword);
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
logoutModalApi.open();
|
||||
openPopover.value = false;
|
||||
}
|
||||
|
||||
function handleSubmitLogout() {
|
||||
emit('logout');
|
||||
logoutModalApi.close();
|
||||
}
|
||||
|
||||
// 设置 - 打开偏好设置抽屉
|
||||
function handleOpenSettings() {
|
||||
refPreferences.value?.open();
|
||||
}
|
||||
|
||||
if (enableShortcutKey.value) {
|
||||
const keys = useMagicKeys();
|
||||
const logoutKey = keys['Alt+KeyQ'];
|
||||
const lockKey = keys['Alt+KeyL'];
|
||||
|
||||
if (logoutKey) {
|
||||
whenever(logoutKey, () => {
|
||||
if (enableLogoutShortcutKey.value) {
|
||||
handleLogout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (lockKey) {
|
||||
whenever(lockKey, () => {
|
||||
if (enableLockScreenShortcutKey.value) {
|
||||
handleOpenLock();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LockModal
|
||||
v-if="showLockInDropdown"
|
||||
:avatar="avatar"
|
||||
:text="text"
|
||||
@submit="handleSubmitLock"
|
||||
/>
|
||||
|
||||
<LogoutModal
|
||||
v-if="showLogoutInDropdown"
|
||||
:cancel-text="$t('common.cancel')"
|
||||
:confirm-text="$t('common.confirm')"
|
||||
:fullscreen-button="false"
|
||||
:title="$t('common.prompt')"
|
||||
centered
|
||||
content-class="px-8 min-h-10"
|
||||
footer-class="border-none mb-3 mr-3"
|
||||
header-class="border-none"
|
||||
>
|
||||
{{ $t('ui.widgets.logoutTip') }}
|
||||
</LogoutModal>
|
||||
|
||||
<Preferences
|
||||
v-if="preferencesButtonPosition.userDropdown"
|
||||
ref="refPreferences"
|
||||
@@ -159,7 +334,97 @@ function handleOpenSettings() {
|
||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
||||
{{ menu.text }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator v-if="preferencesButtonPosition.userDropdown" />
|
||||
<template v-if="showLockInDropdown || showLogoutInDropdown">
|
||||
<DropdownMenuSeparator v-if="showLockInDropdown" />
|
||||
<DropdownMenuItem
|
||||
v-if="showLockInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@click="handleOpenLock"
|
||||
>
|
||||
<LockKeyhole class="mr-2 size-4" />
|
||||
{{ $t('ui.widgets.lockScreen.title') }}
|
||||
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
|
||||
{{ altView }} L
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator v-if="showLogoutInDropdown" />
|
||||
<DropdownMenuItem
|
||||
v-if="showLogoutInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<LogOut class="mr-2 size-4" />
|
||||
{{ $t('common.logout') }}
|
||||
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
|
||||
{{ altView }} Q
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
showGlobalSearchInDropdown ||
|
||||
showThemeToggleInDropdown ||
|
||||
showLanguageToggleInDropdown ||
|
||||
showTimezoneInDropdown ||
|
||||
showFullscreenInDropdown ||
|
||||
showNotificationInDropdown ||
|
||||
showRefreshInDropdown
|
||||
"
|
||||
>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
v-if="showGlobalSearchInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<Search class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.globalSearch') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showThemeToggleInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<Sun class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.themeToggle') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showLanguageToggleInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<Languages class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.languageToggle') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showTimezoneInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<SunMoon class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.timezone') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showFullscreenInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<Fullscreen class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.fullscreen') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showNotificationInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<Bell class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.notification') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showRefreshInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
>
|
||||
<RotateCw class="mr-2 size-4" />
|
||||
{{ $t('preferences.widget.refresh') }}
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
<DropdownMenuSeparator
|
||||
v-if="hasAnyInDropdown || preferencesButtonPosition.userDropdown"
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
v-if="preferencesButtonPosition.userDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"showSearchPanel": "Show search panel",
|
||||
"hideSearchPanel": "Hide search panel"
|
||||
"hideSearchPanel": "Hide search panel",
|
||||
"notShow": "Don't Show"
|
||||
}
|
||||
|
||||
@@ -193,14 +193,26 @@
|
||||
"widget": {
|
||||
"title": "Widget",
|
||||
"globalSearch": "Enable Global Search",
|
||||
"globalSearchPosition": "Global Search Position",
|
||||
"fullscreen": "Enable Fullscreen",
|
||||
"fullscreenPosition": "Fullscreen Position",
|
||||
"themeToggle": "Enable Theme Toggle",
|
||||
"themeTogglePosition": "Theme Toggle Position",
|
||||
"languageToggle": "Enable Language Toggle",
|
||||
"languageTogglePosition": "Language Toggle Position",
|
||||
"notification": "Enable Notification",
|
||||
"notificationPosition": "Notification Position",
|
||||
"sidebarToggle": "Enable Sidebar Toggle",
|
||||
"lockScreen": "Enable Lock Screen",
|
||||
"lockScreenPosition": "Lock Screen Button Position",
|
||||
"logoutButtonPosition": "Logout Button Position",
|
||||
"header": "Header",
|
||||
"userDropdown": "User Dropdown",
|
||||
"hidden": "Hidden",
|
||||
"refresh": "Enable Refresh",
|
||||
"timezone": "Enable Timezone"
|
||||
"refreshPosition": "Refresh Position",
|
||||
"timezone": "Enable Timezone",
|
||||
"timezonePosition": "Timezone Position"
|
||||
},
|
||||
"antd": {
|
||||
"tabLabel": "Antd Extension",
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"showSearchPanel": "显示搜索面板",
|
||||
"hideSearchPanel": "隐藏搜索面板"
|
||||
"hideSearchPanel": "隐藏搜索面板",
|
||||
"notShow": "不显示"
|
||||
}
|
||||
|
||||
@@ -193,14 +193,26 @@
|
||||
"widget": {
|
||||
"title": "小部件",
|
||||
"globalSearch": "启用全局搜索",
|
||||
"globalSearchPosition": "全局搜索位置",
|
||||
"fullscreen": "启用全屏",
|
||||
"fullscreenPosition": "全屏按钮位置",
|
||||
"themeToggle": "启用主题切换",
|
||||
"themeTogglePosition": "主题切换位置",
|
||||
"languageToggle": "启用语言切换",
|
||||
"languageTogglePosition": "语言切换位置",
|
||||
"notification": "启用通知",
|
||||
"notificationPosition": "通知位置",
|
||||
"sidebarToggle": "启用侧边栏切换",
|
||||
"lockScreen": "启用锁屏",
|
||||
"lockScreenPosition": "锁屏按钮位置",
|
||||
"logoutButtonPosition": "退出按钮位置",
|
||||
"header": "顶栏",
|
||||
"userDropdown": "用户下拉窗",
|
||||
"hidden": "已隐藏",
|
||||
"refresh": "启用刷新",
|
||||
"timezone": "启用时区"
|
||||
"refreshPosition": "刷新按钮位置",
|
||||
"timezone": "启用时区",
|
||||
"timezonePosition": "时区按钮位置"
|
||||
},
|
||||
"antd": {
|
||||
"tabLabel": "Antd 拓展",
|
||||
|
||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@@ -524,6 +524,10 @@ overrides:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
sortablejs:
|
||||
specifier: 'catalog:'
|
||||
version: 1.15.7
|
||||
devDependencies:
|
||||
'@changesets/changelog-github':
|
||||
specifier: 'catalog:'
|
||||
@@ -537,6 +541,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 25.9.4
|
||||
'@types/sortablejs':
|
||||
specifier: 'catalog:'
|
||||
version: 1.15.9
|
||||
'@vben/commitlint-config':
|
||||
specifier: workspace:*
|
||||
version: link:internal/lint-configs/commitlint-config
|
||||
@@ -1000,14 +1007,14 @@ importers:
|
||||
version: 2.10.0(vue@3.5.38(typescript@6.0.3))
|
||||
vitepress-plugin-group-icons:
|
||||
specifier: 'catalog:'
|
||||
version: 1.7.5(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
|
||||
version: 1.7.5(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
|
||||
devDependencies:
|
||||
'@nolebase/vitepress-plugin-git-changelog':
|
||||
specifier: 'catalog:'
|
||||
version: 2.18.2(vitepress@2.0.0-alpha.17(@types/node@26.0.0)(async-validator@4.2.5)(axios@1.18.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.101.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.3.1(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
|
||||
version: 4.3.1(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))
|
||||
'@vben/tailwind-config':
|
||||
specifier: workspace:*
|
||||
version: link:../internal/tailwind-config
|
||||
@@ -1016,7 +1023,7 @@ importers:
|
||||
version: link:../internal/vite-config
|
||||
'@vite-pwa/vitepress':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
|
||||
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
|
||||
vitepress:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0-alpha.17(@types/node@26.0.0)(async-validator@4.2.5)(axios@1.18.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.101.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0)
|
||||
@@ -14437,6 +14444,13 @@ snapshots:
|
||||
tailwindcss: 4.3.1
|
||||
vite: 8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
|
||||
|
||||
'@tailwindcss/vite@4.3.1(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.3.1
|
||||
'@tailwindcss/oxide': 4.3.1
|
||||
tailwindcss: 4.3.1
|
||||
vite: 8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
|
||||
|
||||
'@tanstack/match-sorter-utils@8.19.4':
|
||||
dependencies:
|
||||
remove-accents: 0.5.0
|
||||
@@ -15149,9 +15163,9 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
|
||||
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
|
||||
dependencies:
|
||||
vite-plugin-pwa: 1.3.0(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
|
||||
vite-plugin-pwa: 1.3.0(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3))':
|
||||
dependencies:
|
||||
@@ -20568,6 +20582,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
pretty-bytes: 6.1.1
|
||||
tinyglobby: 0.2.17
|
||||
vite: 8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
|
||||
workbox-build: 7.4.1
|
||||
workbox-window: 7.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.1.3(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.3(vue@3.5.38(typescript@6.0.3))
|
||||
@@ -20670,13 +20695,13 @@ snapshots:
|
||||
terser: 5.48.0
|
||||
yaml: 2.9.0
|
||||
|
||||
vitepress-plugin-group-icons@1.7.5(vite@8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)):
|
||||
vitepress-plugin-group-icons@1.7.5(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@iconify-json/logos': 1.2.11
|
||||
'@iconify-json/vscode-icons': 1.2.58
|
||||
'@iconify/utils': 3.1.3
|
||||
optionalDependencies:
|
||||
vite: 8.0.10(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
|
||||
vite: 8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
|
||||
|
||||
vitepress@2.0.0-alpha.17(@types/node@26.0.0)(async-validator@4.2.5)(axios@1.18.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.101.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0):
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user