mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
feat: allow keep open after clicking menu item
This commit is contained in:
parent
aaf09d3f81
commit
582107f0c6
@ -165,6 +165,7 @@ const AddFieldButtonCore: React.FC<AddFieldButtonProps> = ({
|
|||||||
items={items ?? fieldItems}
|
items={items ?? fieldItems}
|
||||||
onModelCreated={onModelCreated}
|
onModelCreated={onModelCreated}
|
||||||
onSubModelAdded={onSubModelAdded}
|
onSubModelAdded={onSubModelAdded}
|
||||||
|
keepDropdownOpen
|
||||||
>
|
>
|
||||||
{children || defaultChildren}
|
{children || defaultChildren}
|
||||||
</AddSubModelButton>
|
</AddSubModelButton>
|
||||||
|
@ -119,6 +119,10 @@ export interface SubModelItem {
|
|||||||
* 搜索占位符文本(仅对启用搜索的 group 有效)
|
* 搜索占位符文本(仅对启用搜索的 group 有效)
|
||||||
*/
|
*/
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
|
/**
|
||||||
|
* 点击后是否保持下拉菜单打开状态
|
||||||
|
*/
|
||||||
|
keepDropdownOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddSubModelButtonProps {
|
interface AddSubModelButtonProps {
|
||||||
@ -160,6 +164,13 @@ interface AddSubModelButtonProps {
|
|||||||
* 按钮文本,默认为 "Add"
|
* 按钮文本,默认为 "Add"
|
||||||
*/
|
*/
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局设置:点击菜单项后是否保持下拉菜单打开状态
|
||||||
|
* 如果设置为 true,所有菜单项点击后都不会关闭下拉菜单
|
||||||
|
* 单个菜单项的 keepDropdownOpen 属性会覆盖此全局设置
|
||||||
|
*/
|
||||||
|
keepDropdownOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,6 +255,7 @@ const AddSubModelButtonCore = function AddSubModelButton({
|
|||||||
onModelCreated,
|
onModelCreated,
|
||||||
onSubModelAdded,
|
onSubModelAdded,
|
||||||
children = 'Add',
|
children = 'Add',
|
||||||
|
keepDropdownOpen = false,
|
||||||
}: AddSubModelButtonProps) {
|
}: AddSubModelButtonProps) {
|
||||||
// 构建上下文对象,从 flowEngine 的全局上下文中获取服务
|
// 构建上下文对象,从 flowEngine 的全局上下文中获取服务
|
||||||
const buildContext = useMemo((): AddSubModelContext => {
|
const buildContext = useMemo((): AddSubModelContext => {
|
||||||
@ -304,7 +316,17 @@ const AddSubModelButtonCore = function AddSubModelButton({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <LazyDropdown menu={{ items: transformItems(items, buildContext), onClick }}>{children}</LazyDropdown>;
|
return (
|
||||||
|
<LazyDropdown
|
||||||
|
menu={{
|
||||||
|
items: transformItems(items, buildContext),
|
||||||
|
onClick,
|
||||||
|
keepDropdownOpen,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LazyDropdown>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSubModelButton = withFlowDesignMode(AddSubModelButtonCore);
|
export const AddSubModelButton = withFlowDesignMode(AddSubModelButtonCore);
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Dropdown, DropdownProps, Empty, Input, InputProps, Spin } from 'antd';
|
import { Dropdown, DropdownProps, Empty, Input, InputProps, Spin } from 'antd';
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useFlowModel } from '../../hooks';
|
|
||||||
import { useFlowEngine } from '../../provider';
|
import { useFlowEngine } from '../../provider';
|
||||||
|
|
||||||
// ==================== Types ====================
|
// ==================== Types ====================
|
||||||
@ -22,6 +21,7 @@ export type Item = {
|
|||||||
children?: Item[] | (() => Item[] | Promise<Item[]>);
|
children?: Item[] | (() => Item[] | Promise<Item[]>);
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
|
keepDropdownOpen?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +29,16 @@ export type ItemsType = Item[] | (() => Item[] | Promise<Item[]>);
|
|||||||
|
|
||||||
interface LazyDropdownMenuProps extends Omit<DropdownProps['menu'], 'items'> {
|
interface LazyDropdownMenuProps extends Omit<DropdownProps['menu'], 'items'> {
|
||||||
items: ItemsType;
|
items: ItemsType;
|
||||||
|
keepDropdownOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtendedMenuInfo {
|
||||||
|
key: string;
|
||||||
|
keyPath: string[];
|
||||||
|
item: any;
|
||||||
|
domEvent: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;
|
||||||
|
originalItem: Item;
|
||||||
|
keepDropdownOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Custom Hooks ====================
|
// ==================== Custom Hooks ====================
|
||||||
@ -102,6 +112,34 @@ const useAsyncMenuItems = (menuVisible: boolean, rootItems: Item[]) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理保持下拉菜单打开状态的逻辑
|
||||||
|
*/
|
||||||
|
const useKeepDropdownOpen = () => {
|
||||||
|
const shouldKeepOpenRef = useRef(false);
|
||||||
|
const [forceKeepOpen, setForceKeepOpen] = useState(false);
|
||||||
|
|
||||||
|
const requestKeepOpen = useCallback(() => {
|
||||||
|
shouldKeepOpenRef.current = true;
|
||||||
|
setForceKeepOpen(true);
|
||||||
|
|
||||||
|
// 使用 requestAnimationFrame 来确保在下一个渲染周期后重置
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
shouldKeepOpenRef.current = false;
|
||||||
|
setForceKeepOpen(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const shouldPreventClose = useCallback(() => {
|
||||||
|
return shouldKeepOpenRef.current || forceKeepOpen;
|
||||||
|
}, [forceKeepOpen]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestKeepOpen,
|
||||||
|
shouldPreventClose,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理菜单搜索状态
|
* 处理菜单搜索状态
|
||||||
*/
|
*/
|
||||||
@ -269,6 +307,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
// 使用自定义 hooks
|
// 使用自定义 hooks
|
||||||
const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems(menuVisible, rootItems);
|
const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems(menuVisible, rootItems);
|
||||||
const { searchValues, isSearching, updateSearchValue } = useMenuSearch();
|
const { searchValues, isSearching, updateSearchValue } = useMenuSearch();
|
||||||
|
const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
|
||||||
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
||||||
|
|
||||||
// 加载根 items,支持同步/异步函数
|
// 加载根 items,支持同步/异步函数
|
||||||
@ -376,10 +415,23 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
if (children) {
|
if (children) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
menu.onClick?.({
|
|
||||||
|
// 检查是否应该保持下拉菜单打开
|
||||||
|
const itemShouldKeepOpen = item.keepDropdownOpen ?? menu.keepDropdownOpen ?? false;
|
||||||
|
|
||||||
|
// 如果需要保持菜单打开,请求保持打开状态
|
||||||
|
if (itemShouldKeepOpen) {
|
||||||
|
requestKeepOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendedInfo: ExtendedMenuInfo = {
|
||||||
...info,
|
...info,
|
||||||
|
item: info.item || item,
|
||||||
originalItem: item,
|
originalItem: item,
|
||||||
} as any);
|
keepDropdownOpen: itemShouldKeepOpen,
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.onClick?.(extendedInfo);
|
||||||
},
|
},
|
||||||
onMouseEnter: () => {
|
onMouseEnter: () => {
|
||||||
setOpenKeys((prev) => {
|
setOpenKeys((prev) => {
|
||||||
@ -461,9 +513,16 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onOpenChange={(visible) => {
|
onOpenChange={(visible) => {
|
||||||
|
// 阻止在搜索时关闭菜单
|
||||||
if (!visible && isSearching) {
|
if (!visible && isSearching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 阻止在需要保持打开时关闭菜单
|
||||||
|
if (!visible && shouldPreventClose()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setMenuVisible(visible);
|
setMenuVisible(visible);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user