Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/access",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/common-ui",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,21 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthenticationProps } from './types';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben-core/popup-ui';
|
||||
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props extends AuthenticationProps {
|
||||
avatar?: string;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginExpiredModal',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
avatar: '',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const open = defineModel<boolean>('open');
|
||||
@@ -28,6 +30,26 @@ watch(
|
||||
modalApi.setState({ isOpen: val });
|
||||
},
|
||||
);
|
||||
|
||||
const getZIndex = computed(() => {
|
||||
return props.zIndex || calcZIndex();
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取最大的zIndex值
|
||||
*/
|
||||
function calcZIndex() {
|
||||
let maxZ = 0;
|
||||
const elements = document.querySelectorAll('*');
|
||||
[...elements].forEach((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const zIndex = style.getPropertyValue('z-index');
|
||||
if (zIndex && !Number.isNaN(Number.parseInt(zIndex))) {
|
||||
maxZ = Math.max(maxZ, Number.parseInt(zIndex));
|
||||
}
|
||||
});
|
||||
return maxZ + 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,6 +61,7 @@ watch(
|
||||
:footer="false"
|
||||
:fullscreen-button="false"
|
||||
:header="false"
|
||||
:z-index="getZIndex"
|
||||
class="border-none px-10 py-6 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset]"
|
||||
>
|
||||
<VbenAvatar :src="avatar" class="mx-auto mb-6 size-20" />
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/hooks",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"@vben/stores": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
"watermark-js-plus": "catalog:"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
export * from './use-app-config';
|
||||
export * from './use-content-maximize';
|
||||
export * from './use-design-tokens';
|
||||
export * from './use-hover-toggle';
|
||||
export * from './use-pagination';
|
||||
export * from './use-refresh';
|
||||
export * from './use-tabs';
|
||||
|
63
packages/effects/hooks/src/use-hover-toggle.ts
Normal file
63
packages/effects/hooks/src/use-hover-toggle.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Arrayable, MaybeElementRef } from '@vueuse/core';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { isFunction } from '@vben/utils';
|
||||
|
||||
import { useMouseInElement } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
|
||||
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
|
||||
* @param delay 延迟更新状态的时间
|
||||
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
|
||||
*/
|
||||
export function useHoverToggle(
|
||||
refElement: Arrayable<MaybeElementRef>,
|
||||
delay: (() => number) | number = 500,
|
||||
) {
|
||||
const isOutsides: Array<Ref<boolean>> = [];
|
||||
const value = ref(false);
|
||||
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||
const refs = Array.isArray(refElement) ? refElement : [refElement];
|
||||
refs.forEach((refEle) => {
|
||||
const listener = useMouseInElement(refEle, { handleOutside: true });
|
||||
isOutsides.push(listener.isOutside);
|
||||
});
|
||||
const isOutsideAll = computed(() => isOutsides.every((v) => v.value));
|
||||
|
||||
function setValueDelay(val: boolean) {
|
||||
timer.value && clearTimeout(timer.value);
|
||||
timer.value = setTimeout(
|
||||
() => {
|
||||
value.value = val;
|
||||
timer.value = undefined;
|
||||
},
|
||||
isFunction(delay) ? delay() : delay,
|
||||
);
|
||||
}
|
||||
|
||||
const watcher = watch(
|
||||
isOutsideAll,
|
||||
(val) => {
|
||||
setValueDelay(!val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const controller = {
|
||||
enable() {
|
||||
watcher.resume();
|
||||
},
|
||||
disable() {
|
||||
watcher.pause();
|
||||
},
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
timer.value && clearTimeout(timer.value);
|
||||
});
|
||||
|
||||
return [value, controller] as [typeof value, typeof controller];
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/layouts",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -103,6 +103,7 @@ const {
|
||||
|
||||
const {
|
||||
handleMenuSelect,
|
||||
handleMenuOpen,
|
||||
headerActive,
|
||||
headerMenus,
|
||||
sidebarActive,
|
||||
@@ -260,6 +261,7 @@ const headerSlots = computed(() => {
|
||||
:rounded="isMenuRounded"
|
||||
:theme="sidebarTheme"
|
||||
mode="vertical"
|
||||
@open="handleMenuOpen"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -14,12 +14,17 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
open: [string, string[]];
|
||||
select: [string, string?];
|
||||
}>();
|
||||
|
||||
function handleMenuSelect(key: string) {
|
||||
emit('select', key, props.mode);
|
||||
}
|
||||
|
||||
function handleMenuOpen(key: string, path: string[]) {
|
||||
emit('open', key, path);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,6 +37,7 @@ function handleMenuSelect(key: string) {
|
||||
:mode="mode"
|
||||
:rounded="rounded"
|
||||
:theme="theme"
|
||||
@open="handleMenuOpen"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -15,6 +15,9 @@ function useExtraMenu() {
|
||||
|
||||
const menus = computed(() => accessStore.accessMenus);
|
||||
|
||||
/** 记录当前顶级菜单下哪个子菜单最后激活 */
|
||||
const defaultSubMap = new Map<string, string>();
|
||||
|
||||
const route = useRoute();
|
||||
const extraMenus = ref<MenuRecordRaw[]>([]);
|
||||
const sidebarExtraVisible = ref<boolean>(false);
|
||||
@@ -32,6 +35,12 @@ function useExtraMenu() {
|
||||
sidebarExtraVisible.value = hasChildren;
|
||||
if (!hasChildren) {
|
||||
await navigation(menu.path);
|
||||
} else if (preferences.sidebar.autoActivateChild) {
|
||||
await navigation(
|
||||
defaultSubMap.has(menu.path)
|
||||
? (defaultSubMap.get(menu.path) as string)
|
||||
: menu.path,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,6 +98,7 @@ function useExtraMenu() {
|
||||
menus.value,
|
||||
currentPath,
|
||||
);
|
||||
if (rootMenuPath) defaultSubMap.set(rootMenuPath, currentPath);
|
||||
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
|
||||
extraMenus.value = rootMenu?.children ?? [];
|
||||
},
|
||||
|
@@ -15,7 +15,8 @@ function useMixedMenu() {
|
||||
const route = useRoute();
|
||||
const splitSideMenus = ref<MenuRecordRaw[]>([]);
|
||||
const rootMenuPath = ref<string>('');
|
||||
|
||||
/** 记录当前顶级菜单下哪个子菜单最后激活 */
|
||||
const defaultSubMap = new Map<string, string>();
|
||||
const { isMixedNav } = usePreferences();
|
||||
|
||||
const needSplit = computed(
|
||||
@@ -86,6 +87,25 @@ function useMixedMenu() {
|
||||
splitSideMenus.value = rootMenu?.children ?? [];
|
||||
if (splitSideMenus.value.length === 0) {
|
||||
navigation(key);
|
||||
} else if (rootMenu && preferences.sidebar.autoActivateChild) {
|
||||
navigation(
|
||||
defaultSubMap.has(rootMenu.path)
|
||||
? (defaultSubMap.get(rootMenu.path) as string)
|
||||
: rootMenu.path,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 侧边菜单展开事件
|
||||
* @param key 路由路径
|
||||
* @param parentsPath 父级路径
|
||||
*/
|
||||
const handleMenuOpen = (key: string, parentsPath: string[]) => {
|
||||
if (parentsPath.length <= 1 && preferences.sidebar.autoActivateChild) {
|
||||
navigation(
|
||||
defaultSubMap.has(key) ? (defaultSubMap.get(key) as string) : key,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,6 +127,8 @@ function useMixedMenu() {
|
||||
(path) => {
|
||||
const currentPath = (route?.meta?.activePath as string) ?? path;
|
||||
calcSideMenus(currentPath);
|
||||
if (rootMenuPath.value)
|
||||
defaultSubMap.set(rootMenuPath.value, currentPath);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -118,6 +140,7 @@ function useMixedMenu() {
|
||||
|
||||
return {
|
||||
handleMenuSelect,
|
||||
handleMenuOpen,
|
||||
headerActive,
|
||||
headerMenus,
|
||||
sidebarActive,
|
||||
|
@@ -55,6 +55,7 @@ if (!preferences.tabbar.persist) {
|
||||
:show-icon="showIcon"
|
||||
:style-type="preferences.tabbar.styleType"
|
||||
:tabs="currentTabs"
|
||||
:wheelable="preferences.tabbar.wheelable"
|
||||
@close="handleClose"
|
||||
@sort-tabs="tabbarStore.sortTabs"
|
||||
@unpin="unpinTab"
|
||||
|
@@ -1,17 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { LayoutType } from '@vben/types';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import NumberFieldItem from '../number-field-item.vue';
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineProps<{ disabled: boolean }>();
|
||||
defineProps<{ currentLayout?: LayoutType; disabled: boolean }>();
|
||||
|
||||
const sidebarEnable = defineModel<boolean>('sidebarEnable');
|
||||
const sidebarWidth = defineModel<number>('sidebarWidth');
|
||||
const sidebarCollapsedShowTitle = defineModel<boolean>(
|
||||
'sidebarCollapsedShowTitle',
|
||||
);
|
||||
const sidebarAutoActivateChild = defineModel<boolean>(
|
||||
'sidebarAutoActivateChild',
|
||||
);
|
||||
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
|
||||
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,12 +27,32 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
|
||||
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
|
||||
{{ $t('preferences.sidebar.collapsed') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
v-model="sidebarExpandOnHover"
|
||||
:disabled="!sidebarEnable || disabled || !sidebarCollapsed"
|
||||
:tip="$t('preferences.sidebar.expandOnHoverTip')"
|
||||
>
|
||||
{{ $t('preferences.sidebar.expandOnHover') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
v-model="sidebarCollapsedShowTitle"
|
||||
:disabled="!sidebarEnable || disabled || !sidebarCollapsed"
|
||||
>
|
||||
{{ $t('preferences.sidebar.collapsedShowTitle') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
v-model="sidebarAutoActivateChild"
|
||||
:disabled="
|
||||
!sidebarEnable ||
|
||||
!['sidebar-mixed-nav', 'mixed-nav', 'sidebar-nav'].includes(
|
||||
currentLayout as string,
|
||||
) ||
|
||||
disabled
|
||||
"
|
||||
:tip="$t('preferences.sidebar.autoActivateChildTip')"
|
||||
>
|
||||
{{ $t('preferences.sidebar.autoActivateChild') }}
|
||||
</SwitchItem>
|
||||
<NumberFieldItem
|
||||
v-model="sidebarWidth"
|
||||
:disabled="!sidebarEnable || disabled"
|
||||
|
@@ -18,6 +18,7 @@ const tabbarEnable = defineModel<boolean>('tabbarEnable');
|
||||
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
|
||||
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
||||
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
|
||||
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
|
||||
const tabbarStyleType = defineModel<string>('tabbarStyleType');
|
||||
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
|
||||
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
|
||||
@@ -53,6 +54,13 @@ const styleItems = computed((): SelectOption[] => [
|
||||
<SwitchItem v-model="tabbarDraggable" :disabled="!tabbarEnable">
|
||||
{{ $t('preferences.tabbar.draggable') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
v-model="tabbarWheelable"
|
||||
:disabled="!tabbarEnable"
|
||||
:tip="$t('preferences.tabbar.wheelableTip')"
|
||||
>
|
||||
{{ $t('preferences.tabbar.wheelable') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
|
||||
{{ $t('preferences.tabbar.icon') }}
|
||||
</SwitchItem>
|
||||
|
@@ -8,8 +8,9 @@ defineOptions({
|
||||
name: 'PreferenceSwitchItem',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<{ disabled?: boolean }>(), {
|
||||
withDefaults(defineProps<{ disabled?: boolean; tip?: string }>(), {
|
||||
disabled: false,
|
||||
tip: '',
|
||||
});
|
||||
|
||||
const checked = defineModel<boolean>();
|
||||
@@ -32,11 +33,17 @@ function handleClick() {
|
||||
<span class="flex items-center text-sm">
|
||||
<slot></slot>
|
||||
|
||||
<VbenTooltip v-if="slots.tip" side="bottom">
|
||||
<VbenTooltip v-if="slots.tip || tip" side="bottom">
|
||||
<template #trigger>
|
||||
<CircleHelp class="ml-1 size-3 cursor-help" />
|
||||
</template>
|
||||
<slot name="tip"></slot>
|
||||
<slot name="tip">
|
||||
<template v-if="tip">
|
||||
<p v-for="(line, index) in tip.split('\n')" :key="index">
|
||||
{{ line }}
|
||||
</p>
|
||||
</template>
|
||||
</slot>
|
||||
</VbenTooltip>
|
||||
</span>
|
||||
<span v-if="$slots.shortcut" class="ml-auto mr-2 text-xs opacity-60">
|
||||
|
@@ -87,6 +87,10 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
|
||||
const sidebarCollapsedShowTitle = defineModel<boolean>(
|
||||
'sidebarCollapsedShowTitle',
|
||||
);
|
||||
const sidebarAutoActivateChild = defineModel<boolean>(
|
||||
'sidebarAutoActivateChild',
|
||||
);
|
||||
const SidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
|
||||
|
||||
const headerEnable = defineModel<boolean>('headerEnable');
|
||||
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
|
||||
@@ -105,6 +109,7 @@ const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
|
||||
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
|
||||
const tabbarPersist = defineModel<boolean>('tabbarPersist');
|
||||
const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
|
||||
const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
|
||||
const tabbarStyleType = defineModel<string>('tabbarStyleType');
|
||||
|
||||
const navigationStyleType = defineModel<NavigationStyleType>(
|
||||
@@ -298,10 +303,13 @@ async function handleReset() {
|
||||
|
||||
<Block :title="$t('preferences.sidebar.title')">
|
||||
<Sidebar
|
||||
v-model:sidebar-auto-activate-child="sidebarAutoActivateChild"
|
||||
v-model:sidebar-collapsed="sidebarCollapsed"
|
||||
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
|
||||
v-model:sidebar-enable="sidebarEnable"
|
||||
v-model:sidebar-expand-on-hover="SidebarExpandOnHover"
|
||||
v-model:sidebar-width="sidebarWidth"
|
||||
:current-layout="appLayout"
|
||||
:disabled="!isSideMode"
|
||||
/>
|
||||
</Block>
|
||||
@@ -345,6 +353,7 @@ async function handleReset() {
|
||||
v-model:tabbar-show-maximize="tabbarShowMaximize"
|
||||
v-model:tabbar-show-more="tabbarShowMore"
|
||||
v-model:tabbar-style-type="tabbarStyleType"
|
||||
v-model:tabbar-wheelable="tabbarWheelable"
|
||||
/>
|
||||
</Block>
|
||||
<Block :title="$t('preferences.widget.title')">
|
||||
|
@@ -2,8 +2,9 @@
|
||||
import type { AnyFunction } from '@vben/types';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { useHoverToggle } from '@vben/hooks';
|
||||
import { LockKeyhole, LogOut } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
@@ -53,6 +54,10 @@ interface Props {
|
||||
* 文本
|
||||
*/
|
||||
text?: string;
|
||||
/** 触发方式 */
|
||||
trigger?: 'both' | 'click' | 'hover';
|
||||
/** hover触发时,延迟响应的时间 */
|
||||
hoverDelay?: number;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
@@ -67,10 +72,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showShortcutKey: true,
|
||||
tagText: '',
|
||||
text: '',
|
||||
trigger: 'click',
|
||||
hoverDelay: 500,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ logout: [] }>();
|
||||
const openPopover = ref(false);
|
||||
|
||||
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
|
||||
usePreferences();
|
||||
@@ -84,6 +90,27 @@ const [LogoutModal, logoutModalApi] = useVbenModal({
|
||||
},
|
||||
});
|
||||
|
||||
const refTrigger = useTemplateRef('refTrigger');
|
||||
const refContent = useTemplateRef('refContent');
|
||||
const [openPopover, hoverWatcher] = useHoverToggle(
|
||||
[refTrigger, refContent],
|
||||
() => props.hoverDelay,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.trigger === 'hover' || props.trigger === 'both',
|
||||
(val) => {
|
||||
if (val) {
|
||||
hoverWatcher.enable();
|
||||
} else {
|
||||
hoverWatcher.disable();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
||||
|
||||
const enableLogoutShortcutKey = computed(() => {
|
||||
@@ -155,8 +182,8 @@ if (enableShortcutKey.value) {
|
||||
{{ $t('ui.widgets.logoutTip') }}
|
||||
</LogoutModal>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenu v-model:open="openPopover">
|
||||
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
||||
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
|
||||
<div class="hover:text-accent-foreground flex-center">
|
||||
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
|
||||
@@ -164,64 +191,66 @@ if (enableShortcutKey.value) {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
|
||||
<DropdownMenuLabel class="flex items-center p-3">
|
||||
<VbenAvatar
|
||||
:alt="text"
|
||||
:src="avatar"
|
||||
class="size-12"
|
||||
dot
|
||||
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
||||
/>
|
||||
<div class="ml-2 w-full">
|
||||
<div
|
||||
v-if="tagText || text || $slots.tagText"
|
||||
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
||||
>
|
||||
{{ text }}
|
||||
<slot name="tagText">
|
||||
<Badge v-if="tagText" class="ml-2 text-green-400">
|
||||
{{ tagText }}
|
||||
</Badge>
|
||||
</slot>
|
||||
<div ref="refContent">
|
||||
<DropdownMenuLabel class="flex items-center p-3">
|
||||
<VbenAvatar
|
||||
:alt="text"
|
||||
:src="avatar"
|
||||
class="size-12"
|
||||
dot
|
||||
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
||||
/>
|
||||
<div class="ml-2 w-full">
|
||||
<div
|
||||
v-if="tagText || text || $slots.tagText"
|
||||
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
||||
>
|
||||
{{ text }}
|
||||
<slot name="tagText">
|
||||
<Badge v-if="tagText" class="ml-2 text-green-400">
|
||||
{{ tagText }}
|
||||
</Badge>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator v-if="menus?.length" />
|
||||
<DropdownMenuItem
|
||||
v-for="menu in menus"
|
||||
:key="menu.text"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@click="menu.handler"
|
||||
>
|
||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
||||
{{ menu.text }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
v-if="preferences.widget.lockScreen"
|
||||
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="preferences.widget.lockScreen" />
|
||||
<DropdownMenuItem
|
||||
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>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator v-if="menus?.length" />
|
||||
<DropdownMenuItem
|
||||
v-for="menu in menus"
|
||||
:key="menu.text"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@click="menu.handler"
|
||||
>
|
||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
||||
{{ menu.text }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
v-if="preferences.widget.lockScreen"
|
||||
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="preferences.widget.lockScreen" />
|
||||
<DropdownMenuItem
|
||||
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>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/plugins",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,6 +8,7 @@ import { toRaw } from 'vue';
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
bindMethods,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
mergeWithArrayOverride,
|
||||
StateHandler,
|
||||
@@ -20,6 +21,7 @@ function getDefaultState(): VxeGridProps {
|
||||
gridOptions: {},
|
||||
gridEvents: {},
|
||||
formOptions: undefined,
|
||||
showSearchForm: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,6 +110,16 @@ export class VxeGridApi {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSearchForm(show?: boolean) {
|
||||
this.setState({
|
||||
showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm,
|
||||
});
|
||||
// nextTick(() => {
|
||||
// this.grid.recalculate();
|
||||
// });
|
||||
return this.state?.showSearchForm;
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.isMounted = false;
|
||||
this.stateHandler.reset();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export { setupVbenVxeTable } from './init';
|
||||
export type { VxeTableGridOptions } from './types';
|
||||
export * from './use-vxe-grid';
|
||||
export { default as VbenVxeGrid } from './use-vxe-grid.vue';
|
||||
export type { VxeGridDefines, VxeGridListeners, VxeGridProps } from 'vxe-table';
|
||||
|
@@ -2,6 +2,7 @@ import type { ClassType, DeepPartial } from '@vben/types';
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
import type {
|
||||
VxeGridListeners,
|
||||
VxeGridPropTypes,
|
||||
VxeGridProps as VxeTableGridProps,
|
||||
VxeUIExport,
|
||||
} from 'vxe-table';
|
||||
@@ -18,6 +19,16 @@ export interface VxePaginationInfo {
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ToolbarConfigOptions extends VxeGridPropTypes.ToolbarConfig {
|
||||
/** 是否显示切换搜索表单的按钮 */
|
||||
search?: boolean;
|
||||
}
|
||||
|
||||
export interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {
|
||||
/** 工具栏配置 */
|
||||
toolbarConfig?: ToolbarConfigOptions;
|
||||
}
|
||||
|
||||
export interface VxeGridProps {
|
||||
/**
|
||||
* 标题
|
||||
@@ -38,7 +49,7 @@ export interface VxeGridProps {
|
||||
/**
|
||||
* vxe-grid 配置
|
||||
*/
|
||||
gridOptions?: DeepPartial<VxeTableGridProps>;
|
||||
gridOptions?: DeepPartial<VxeTableGridOptions>;
|
||||
/**
|
||||
* vxe-grid 事件
|
||||
*/
|
||||
@@ -47,6 +58,10 @@ export interface VxeGridProps {
|
||||
* 表单配置
|
||||
*/
|
||||
formOptions?: VbenFormProps;
|
||||
/**
|
||||
* 显示搜索表单
|
||||
*/
|
||||
showSearchForm?: boolean;
|
||||
}
|
||||
|
||||
export type ExtendedVxeGridApi = {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
import type {
|
||||
VxeGridDefines,
|
||||
VxeGridInstance,
|
||||
VxeGridListeners,
|
||||
VxeGridPropTypes,
|
||||
VxeGridProps as VxeTableGridProps,
|
||||
} from 'vxe-table';
|
||||
|
||||
@@ -57,6 +60,7 @@ const {
|
||||
formOptions,
|
||||
tableTitle,
|
||||
tableTitleHelp,
|
||||
showSearchForm,
|
||||
} = usePriorityValues(props, state);
|
||||
|
||||
const { isMobile } = usePreferences();
|
||||
@@ -105,21 +109,37 @@ const toolbarOptions = computed(() => {
|
||||
const slotActions = slots[TOOLBAR_ACTIONS]?.();
|
||||
const slotTools = slots[TOOLBAR_TOOLS]?.();
|
||||
|
||||
const toolbarConfig: VxeGridPropTypes.ToolbarConfig = {
|
||||
tools:
|
||||
gridOptions.value?.toolbarConfig?.search && !!formOptions.value
|
||||
? [
|
||||
{
|
||||
code: 'search',
|
||||
icon: 'vxe-icon--search',
|
||||
circle: true,
|
||||
status: showSearchForm.value ? 'primary' : undefined,
|
||||
title: $t('common.search'),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
if (!showToolbar.value) {
|
||||
return {};
|
||||
return { toolbarConfig };
|
||||
}
|
||||
|
||||
// if (gridOptions.value?.toolbarConfig?.search) {
|
||||
// }
|
||||
// 强制使用固定的toolbar配置,不允许用户自定义
|
||||
// 减少配置的复杂度,以及后续维护的成本
|
||||
return {
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
...(slotActions || showTableTitle.value
|
||||
? { buttons: TOOLBAR_ACTIONS }
|
||||
: {}),
|
||||
...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),
|
||||
},
|
||||
},
|
||||
toolbarConfig.slots = {
|
||||
...(slotActions || showTableTitle.value
|
||||
? { buttons: TOOLBAR_ACTIONS }
|
||||
: {}),
|
||||
...(slotTools ? { tools: TOOLBAR_TOOLS } : {}),
|
||||
};
|
||||
|
||||
return { toolbarConfig };
|
||||
});
|
||||
|
||||
const options = computed(() => {
|
||||
@@ -175,9 +195,19 @@ const options = computed(() => {
|
||||
return mergedOptions;
|
||||
});
|
||||
|
||||
function onToolbarToolClick(event: VxeGridDefines.ToolbarToolClickEventParams) {
|
||||
if (event.code === 'search') {
|
||||
props.api?.toggleSearchForm?.();
|
||||
}
|
||||
(
|
||||
gridEvents.value?.toolbarToolClick as VxeGridListeners['toolbarToolClick']
|
||||
)?.(event);
|
||||
}
|
||||
|
||||
const events = computed(() => {
|
||||
return {
|
||||
...gridEvents.value,
|
||||
toolbarToolClick: onToolbarToolClick,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -215,7 +245,8 @@ async function init() {
|
||||
const autoLoad = defaultGridOptions.proxyConfig?.autoLoad;
|
||||
const enableProxyConfig = options.value.proxyConfig?.enabled;
|
||||
if (enableProxyConfig && autoLoad) {
|
||||
props.api.reload(formApi.form?.values ?? {});
|
||||
props.api.grid.commitProxy?.('_init', formApi.form?.values ?? {});
|
||||
// props.api.reload(formApi.form?.values ?? {});
|
||||
}
|
||||
|
||||
// form 由 vben-form代替,所以不适配formConfig,这里给出警告
|
||||
@@ -306,7 +337,11 @@ onUnmounted(() => {
|
||||
|
||||
<!-- form表单 -->
|
||||
<template #form>
|
||||
<div v-if="formOptions" class="relative rounded py-3 pb-4">
|
||||
<div
|
||||
v-if="formOptions"
|
||||
v-show="showSearchForm !== false"
|
||||
class="relative rounded py-3 pb-4"
|
||||
>
|
||||
<slot name="form">
|
||||
<Form>
|
||||
<template
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/request",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.1",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
Reference in New Issue
Block a user