fix: dropdown menu responsiveness and improve dialog handling (#8187)
* fix: 修复用户下拉框小部件菜单行点击无响应及弹框闪退 - 主题/时区/全屏/通知菜单行补充 @select 处理器,点击行任意位置即可触发 - 时区设置弹框宿主移出 DropdownMenu(showButton 模式),修复弹框随下拉框关闭而闪退 - 通知组件暴露 toggle 供下拉框调用 - 偏好设置小部件文案精简,positionMap 顺序调整 * feat: draggable-list.vue 直接复用 SelectItem,用法和 tabbar.vue 完全一致
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -60,3 +60,4 @@ vite.config.ts.*
|
|||||||
skills-lock.json
|
skills-lock.json
|
||||||
.atomcode
|
.atomcode
|
||||||
datalog
|
datalog
|
||||||
|
.playwright-mcp
|
||||||
|
|||||||
@@ -43,18 +43,20 @@ const close = () => {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewAll = () => {
|
function handleViewAll() {
|
||||||
emit('viewAll');
|
emit('viewAll');
|
||||||
close();
|
close();
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMakeAll = () => {
|
function handleMakeAll() {
|
||||||
emit('makeAll');
|
emit('makeAll');
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleClear = () => {
|
function handleClear() {
|
||||||
emit('clear');
|
emit('clear');
|
||||||
};
|
}
|
||||||
|
|
||||||
|
defineExpose({ toggle });
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VbenPopover v-model:open="open" content-class="relative right-2 w-90 p-0">
|
<VbenPopover v-model:open="open" content-class="relative right-2 w-90 p-0">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { $t } from '@vben/locales';
|
|||||||
|
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
import SelectItem from './select-item.vue';
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -67,14 +69,13 @@ onUnmounted(() => {
|
|||||||
sortableInstance = null;
|
sortableInstance = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function setPosition(key: string, event: Event) {
|
function setPosition(key: string, value: string | undefined) {
|
||||||
const value = (event.target as HTMLSelectElement).value as
|
if (!value) return;
|
||||||
| 'auto'
|
emit(
|
||||||
| 'fixed'
|
'updatePosition',
|
||||||
| 'header'
|
key,
|
||||||
| 'none'
|
value as 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||||
| 'user-dropdown';
|
);
|
||||||
emit('updatePosition', key, value);
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
emit(
|
emit(
|
||||||
'updateOrder',
|
'updateOrder',
|
||||||
@@ -91,25 +92,21 @@ function setPosition(key: string, event: Event) {
|
|||||||
v-for="item in sortableList"
|
v-for="item in sortableList"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:data-key="item.key"
|
:data-key="item.key"
|
||||||
class="bg-accent flex items-center gap-2 rounded-md px-2 py-1.5"
|
class="bg-accent flex items-center rounded-md pl-2"
|
||||||
>
|
>
|
||||||
<GripVertical
|
<GripVertical
|
||||||
class="drag-handle size-4 shrink-0 cursor-grab text-muted-foreground active:cursor-grabbing"
|
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>
|
<SelectItem
|
||||||
<select
|
:items="item.positionItems ?? positionItems"
|
||||||
:value="item.position"
|
:model-value="item.position"
|
||||||
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
class="min-w-0 flex-1"
|
||||||
@change="(e) => setPosition(item.key, e)"
|
@update:model-value="
|
||||||
|
(v: string | undefined) => setPosition(item.key, v)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<option
|
<span class="truncate">{{ item.label }}</span>
|
||||||
v-for="opt in item.positionItems ?? positionItems"
|
</SelectItem>
|
||||||
:key="opt.value"
|
|
||||||
:value="opt.value"
|
|
||||||
>
|
|
||||||
{{ opt.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hiddenList.length > 0" class="pt-2">
|
<div v-if="hiddenList.length > 0" class="pt-2">
|
||||||
@@ -119,26 +116,22 @@ function setPosition(key: string, event: Event) {
|
|||||||
<div
|
<div
|
||||||
v-for="item in hiddenList"
|
v-for="item in hiddenList"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="flex items-center gap-2 rounded-md px-2 py-1"
|
class="flex items-center rounded-md"
|
||||||
>
|
>
|
||||||
<span
|
<SelectItem
|
||||||
class="text-muted-foreground min-w-0 flex-1 truncate text-sm line-through decoration-dotted"
|
:items="item.positionItems ?? positionItems"
|
||||||
|
:model-value="item.position"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
@update:model-value="
|
||||||
|
(v: string | undefined) => setPosition(item.key, v)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<span
|
||||||
</span>
|
class="text-muted-foreground truncate line-through decoration-dotted"
|
||||||
<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 }}
|
{{ item.label }}
|
||||||
</option>
|
</span>
|
||||||
</select>
|
</SelectItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ const positionMap: Record<string, string> = {
|
|||||||
languageToggle: 'widgetLanguageToggleButtonPosition',
|
languageToggle: 'widgetLanguageToggleButtonPosition',
|
||||||
timezone: 'widgetTimezoneButtonPosition',
|
timezone: 'widgetTimezoneButtonPosition',
|
||||||
fullscreen: 'widgetFullscreenButtonPosition',
|
fullscreen: 'widgetFullscreenButtonPosition',
|
||||||
|
refresh: 'widgetRefreshButtonPosition',
|
||||||
notification: 'widgetNotificationButtonPosition',
|
notification: 'widgetNotificationButtonPosition',
|
||||||
lockScreenBtn: 'widgetLockScreenButtonPosition',
|
lockScreenBtn: 'widgetLockScreenButtonPosition',
|
||||||
logoutBtn: 'widgetLogoutButtonPosition',
|
logoutBtn: 'widgetLogoutButtonPosition',
|
||||||
refresh: 'widgetRefreshButtonPosition',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
withDefaults(defineProps<{ showButton?: boolean }>(), { showButton: true });
|
||||||
|
|
||||||
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
|
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
|
||||||
|
|
||||||
const timezoneStore = useTimezoneStore();
|
const timezoneStore = useTimezoneStore();
|
||||||
@@ -47,17 +49,20 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = () => {
|
function open() {
|
||||||
modalApi.open();
|
modalApi.open();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
|
v-if="showButton"
|
||||||
:tooltip="$t('ui.widgets.timezone.setTimezone')"
|
:tooltip="$t('ui.widgets.timezone.setTimezone')"
|
||||||
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
||||||
@click="handleClick"
|
@click="open"
|
||||||
>
|
>
|
||||||
<TimezoneIcon class="size-4 text-foreground" />
|
<TimezoneIcon class="size-4 text-foreground" />
|
||||||
</VbenIconButton>
|
</VbenIconButton>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { computed, ref, useTemplateRef, watch } from 'vue';
|
|||||||
import { SUPPORT_LANGUAGES } from '@vben/constants';
|
import { SUPPORT_LANGUAGES } from '@vben/constants';
|
||||||
import { useHoverToggle, useRefresh } from '@vben/hooks';
|
import { useHoverToggle, useRefresh } from '@vben/hooks';
|
||||||
import {
|
import {
|
||||||
|
createIconifyIcon,
|
||||||
Languages,
|
Languages,
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
LogOut,
|
LogOut,
|
||||||
@@ -40,7 +41,7 @@ import {
|
|||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useFullscreen, useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
import { GlobalSearch } from '../global-search';
|
import { GlobalSearch } from '../global-search';
|
||||||
import { LockScreenModal } from '../lock-screen';
|
import { LockScreenModal } from '../lock-screen';
|
||||||
@@ -101,9 +102,11 @@ const {
|
|||||||
globalLogoutShortcutKey,
|
globalLogoutShortcutKey,
|
||||||
globalLockScreenShortcutKey,
|
globalLockScreenShortcutKey,
|
||||||
globalSearchShortcutKey,
|
globalSearchShortcutKey,
|
||||||
|
isDark,
|
||||||
preferencesButtonPosition,
|
preferencesButtonPosition,
|
||||||
} = usePreferences();
|
} = usePreferences();
|
||||||
const { refresh } = useRefresh();
|
const { refresh } = useRefresh();
|
||||||
|
const { toggle: toggleFullscreen } = useFullscreen();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const [LockModal, lockModalApi] = useVbenModal({
|
const [LockModal, lockModalApi] = useVbenModal({
|
||||||
connectedComponent: LockScreenModal,
|
connectedComponent: LockScreenModal,
|
||||||
@@ -118,6 +121,10 @@ const refTrigger = useTemplateRef('refTrigger');
|
|||||||
const refContent = useTemplateRef('refContent');
|
const refContent = useTemplateRef('refContent');
|
||||||
const refPreferences = useTemplateRef('refPreferences');
|
const refPreferences = useTemplateRef('refPreferences');
|
||||||
const refGlobalSearch = useTemplateRef('refGlobalSearch');
|
const refGlobalSearch = useTemplateRef('refGlobalSearch');
|
||||||
|
const refTimezone = useTemplateRef('refTimezone');
|
||||||
|
const refNotification = useTemplateRef('refNotification');
|
||||||
|
|
||||||
|
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
|
||||||
const [openPopover, hoverWatcher] = useHoverToggle(
|
const [openPopover, hoverWatcher] = useHoverToggle(
|
||||||
[refTrigger, refContent],
|
[refTrigger, refContent],
|
||||||
() => props.hoverDelay,
|
() => props.hoverDelay,
|
||||||
@@ -254,6 +261,28 @@ function handleGlobalSearch() {
|
|||||||
refGlobalSearch.value?.open();
|
refGlobalSearch.value?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主题切换
|
||||||
|
function handleThemeToggleSelect(event?: Event) {
|
||||||
|
event?.preventDefault();
|
||||||
|
updatePreferences({ theme: { mode: isDark.value ? 'light' : 'dark' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时区
|
||||||
|
function handleTimezoneSelect() {
|
||||||
|
refTimezone.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏切换
|
||||||
|
function handleFullscreenSelect() {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知
|
||||||
|
function handleNotificationSelect(event?: Event) {
|
||||||
|
event?.preventDefault();
|
||||||
|
refNotification.value?.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
// 语言切换 - 阻止 Radix 默认关闭外层 dropdown,就地展开/收起 locale 列表
|
// 语言切换 - 阻止 Radix 默认关闭外层 dropdown,就地展开/收起 locale 列表
|
||||||
const showLanguageList = ref(false);
|
const showLanguageList = ref(false);
|
||||||
function handleLanguageToggleSelect(event?: Event) {
|
function handleLanguageToggleSelect(event?: Event) {
|
||||||
@@ -329,6 +358,12 @@ if (enableShortcutKey.value) {
|
|||||||
:show-button="false"
|
:show-button="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TimezoneButton
|
||||||
|
v-if="showTimezoneInDropdown"
|
||||||
|
ref="refTimezone"
|
||||||
|
:show-button="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenu v-model:open="openPopover" :modal="false">
|
<DropdownMenu v-model:open="openPopover" :modal="false">
|
||||||
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
||||||
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
|
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
|
||||||
@@ -435,6 +470,7 @@ if (enableShortcutKey.value) {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showThemeToggleInDropdown"
|
v-if="showThemeToggleInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleThemeToggleSelect"
|
||||||
>
|
>
|
||||||
<ThemeToggle class="mr-2" />
|
<ThemeToggle class="mr-2" />
|
||||||
{{ $t('preferences.theme.title') }}
|
{{ $t('preferences.theme.title') }}
|
||||||
@@ -468,22 +504,27 @@ if (enableShortcutKey.value) {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showTimezoneInDropdown"
|
v-if="showTimezoneInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleTimezoneSelect"
|
||||||
>
|
>
|
||||||
<TimezoneButton class="mr-2" />
|
<VbenIconButton class="mr-2" @click="handleTimezoneSelect">
|
||||||
|
<TimezoneIcon class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('ui.widgets.timezone.setTimezone') }}
|
{{ $t('ui.widgets.timezone.setTimezone') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showFullscreenInDropdown"
|
v-if="showFullscreenInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleFullscreenSelect"
|
||||||
>
|
>
|
||||||
<VbenFullScreen class="mr-2" />
|
<VbenFullScreen class="mr-2" @click.stop />
|
||||||
{{ $t('preferences.widget.fullscreen') }}
|
{{ $t('preferences.widget.fullscreen') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showNotificationInDropdown"
|
v-if="showNotificationInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleNotificationSelect"
|
||||||
>
|
>
|
||||||
<Notification class="mr-2" />
|
<Notification ref="refNotification" class="mr-2" />
|
||||||
{{ $t('preferences.widget.notification') }}
|
{{ $t('preferences.widget.notification') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -194,24 +194,24 @@
|
|||||||
"title": "小部件",
|
"title": "小部件",
|
||||||
"globalSearch": "全局搜索",
|
"globalSearch": "全局搜索",
|
||||||
"globalSearchPosition": "全局搜索位置",
|
"globalSearchPosition": "全局搜索位置",
|
||||||
"fullscreen": "启用全屏",
|
"fullscreen": "全屏",
|
||||||
"fullscreenPosition": "全屏按钮位置",
|
"fullscreenPosition": "全屏按钮位置",
|
||||||
"themeToggle": "启用主题切换",
|
"themeToggle": "主题切换",
|
||||||
"themeTogglePosition": "主题切换位置",
|
"themeTogglePosition": "主题切换位置",
|
||||||
"languageToggle": "语言切换",
|
"languageToggle": "语言切换",
|
||||||
"languageTogglePosition": "语言切换位置",
|
"languageTogglePosition": "语言切换位置",
|
||||||
"notification": "启用通知",
|
"notification": "通知",
|
||||||
"notificationPosition": "通知位置",
|
"notificationPosition": "通知位置",
|
||||||
"sidebarToggle": "启用侧边栏切换",
|
"sidebarToggle": "启用侧边栏切换",
|
||||||
"lockScreen": "启用锁屏",
|
"lockScreen": "锁屏",
|
||||||
"lockScreenPosition": "锁屏按钮位置",
|
"lockScreenPosition": "锁屏按钮位置",
|
||||||
"logoutButtonPosition": "退出按钮位置",
|
"logoutButtonPosition": "退出按钮位置",
|
||||||
"header": "顶栏",
|
"header": "顶栏",
|
||||||
"userDropdown": "用户下拉窗",
|
"userDropdown": "用户下拉窗",
|
||||||
"hidden": "已隐藏",
|
"hidden": "已隐藏",
|
||||||
"refresh": "启用刷新",
|
"refresh": "刷新",
|
||||||
"refreshPosition": "刷新按钮位置",
|
"refreshPosition": "刷新按钮位置",
|
||||||
"timezone": "启用时区",
|
"timezone": "时区",
|
||||||
"timezonePosition": "时区按钮位置"
|
"timezonePosition": "时区按钮位置"
|
||||||
},
|
},
|
||||||
"antd": {
|
"antd": {
|
||||||
|
|||||||
Reference in New Issue
Block a user