Browse Source

fix: dropdown menu responsiveness and improve dialog handling (#8187)

* fix: 修复用户下拉框小部件菜单行点击无响应及弹框闪退

- 主题/时区/全屏/通知菜单行补充 @select 处理器,点击行任意位置即可触发
- 时区设置弹框宿主移出 DropdownMenu(showButton 模式),修复弹框随下拉框关闭而闪退
- 通知组件暴露 toggle 供下拉框调用
- 偏好设置小部件文案精简,positionMap 顺序调整

* feat: draggable-list.vue 直接复用 SelectItem,用法和 tabbar.vue 完全一致
pull/8188/head^2
xingyu 2 months ago
committed by GitHub
parent
commit
2d82505330
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .gitignore
  2. 14
      packages/effects/layouts/src/widgets/notification/notification.vue
  3. 71
      packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue
  4. 2
      packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue
  5. 11
      packages/effects/layouts/src/widgets/timezone/timezone-button.vue
  6. 49
      packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
  7. 12
      packages/locales/src/langs/zh-CN/preferences.json

1
.gitignore

@ -60,3 +60,4 @@ vite.config.ts.*
skills-lock.json skills-lock.json
.atomcode .atomcode
datalog datalog
.playwright-mcp

14
packages/effects/layouts/src/widgets/notification/notification.vue

@ -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">

71
packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue

@ -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"
{{ item.label }} class="min-w-0 flex-1"
</span> @update:model-value="
<select (v: string | undefined) => setPosition(item.key, v)
: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 <span
v-for="opt in item.positionItems ?? positionItems" class="text-muted-foreground truncate line-through decoration-dotted"
:key="opt.value"
:value="opt.value"
> >
{{ opt.label }} {{ item.label }}
</option> </span>
</select> </SelectItem>
</div> </div>
</div> </div>
</div> </div>

2
packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue

@ -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> = {

11
packages/effects/layouts/src/widgets/timezone/timezone-button.vue

@ -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>

49
packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue

@ -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

12
packages/locales/src/langs/zh-CN/preferences.json

@ -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": {

Loading…
Cancel
Save