mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 11:12:20 +08:00
fix: menus item not shown
This commit is contained in:
parent
bc0394cdb4
commit
49bcc1a38f
@ -72,7 +72,7 @@ const AddActionButtonCore: React.FC<AddActionButtonProps> = ({
|
|||||||
onSubModelAdded,
|
onSubModelAdded,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultChildren = useMemo(() => {
|
const defaultChildren = useMemo(() => {
|
||||||
return <FlowSettingsButton icon={<SettingOutlined />}>{model.translate('Configure fields')}</FlowSettingsButton>;
|
return <FlowSettingsButton icon={<SettingOutlined />}>{model.translate('Configure actions')}</FlowSettingsButton>;
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
const allActionsItems = useMemo(() => {
|
const allActionsItems = useMemo(() => {
|
||||||
|
@ -7,63 +7,21 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dropdown, DropdownProps, Input, Menu, Spin, Empty, InputProps } from 'antd';
|
import { Dropdown, DropdownProps, Input, Spin, Empty, InputProps } from 'antd';
|
||||||
import React, { useEffect, useState, useMemo, useRef, FC } from 'react';
|
import React, { useEffect, useState, useMemo, useRef, FC } from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
import { useFlowModel } from '../../hooks';
|
import { useFlowModel } from '../../hooks';
|
||||||
|
|
||||||
/**
|
// ==================== Types ====================
|
||||||
* 通过鼠标的位置计算出最佳的 dropdown 的高度,以尽量避免出现滚动条
|
|
||||||
* @param deps 类似于 useEffect 的第二个参数,如果不传则默认为 []
|
|
||||||
*/
|
|
||||||
const useNiceDropdownMaxHeight = (deps: any[] = []) => {
|
|
||||||
const heightRef = useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
const { clientY } = e;
|
|
||||||
const h = Math.max(clientY, window.innerHeight - clientY);
|
|
||||||
heightRef.current = h;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousemove', handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return useMemo(() => heightRef.current - 40, deps);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使搜索输入框在显示下拉菜单时自动聚焦,提高用户体验。
|
|
||||||
*
|
|
||||||
* 注意:Input 组件的 autofocus 属性只会在第一次显示下拉菜单时有效,所以这里没有使用它。
|
|
||||||
* @param props
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const SearchInputWithAutoFocus: FC<InputProps & { visible: boolean }> = (props) => {
|
|
||||||
const inputRef = useRef<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current && props.visible) {
|
|
||||||
console.log('Focus input:', inputRef.current.input);
|
|
||||||
inputRef.current.input.focus();
|
|
||||||
}
|
|
||||||
}, [props.visible]);
|
|
||||||
|
|
||||||
return <Input ref={inputRef} {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 菜单项类型定义
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
key?: string;
|
key?: string;
|
||||||
type?: 'group' | 'divider';
|
type?: 'group' | 'divider';
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
children?: Item[] | (() => Item[] | Promise<Item[]>);
|
children?: Item[] | (() => Item[] | Promise<Item[]>);
|
||||||
searchable?: boolean; // group 是否支持搜索
|
searchable?: boolean;
|
||||||
searchPlaceholder?: string; // 搜索占位符
|
searchPlaceholder?: string;
|
||||||
[key: string]: any; // 允许其他属性
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ItemsType = Item[] | (() => Item[] | Promise<Item[]>);
|
export type ItemsType = Item[] | (() => Item[] | Promise<Item[]>);
|
||||||
@ -72,30 +30,24 @@ interface LazyDropdownMenuProps extends Omit<DropdownProps['menu'], 'items'> {
|
|||||||
items: ItemsType;
|
items: ItemsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
// ==================== Custom Hooks ====================
|
||||||
const model = useFlowModel();
|
|
||||||
|
/**
|
||||||
|
* 计算合适的下拉菜单最大高度
|
||||||
|
*/
|
||||||
|
const useNiceDropdownMaxHeight = () => {
|
||||||
|
return useMemo(() => {
|
||||||
|
const maxHeight = Math.min(window.innerHeight * 0.6, 400);
|
||||||
|
return maxHeight;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理异步菜单项加载的逻辑
|
||||||
|
*/
|
||||||
|
const useAsyncMenuItems = (menuVisible: boolean, rootItems: Item[]) => {
|
||||||
const [loadedChildren, setLoadedChildren] = useState<Record<string, Item[]>>({});
|
const [loadedChildren, setLoadedChildren] = useState<Record<string, Item[]>>({});
|
||||||
const [loadingKeys, setLoadingKeys] = useState<Set<string>>(new Set());
|
const [loadingKeys, setLoadingKeys] = useState<Set<string>>(new Set());
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
|
||||||
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
|
||||||
const [rootItems, setRootItems] = useState<Item[]>([]);
|
|
||||||
const [rootLoading, setRootLoading] = useState(false);
|
|
||||||
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
|
|
||||||
const dropdownMaxHeight = useNiceDropdownMaxHeight([menuVisible]);
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const t = model.translate;
|
|
||||||
|
|
||||||
// 清理定时器,避免内存泄露
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (searchTimeoutRef.current) {
|
|
||||||
clearTimeout(searchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
|
|
||||||
|
|
||||||
const handleLoadChildren = async (keyPath: string, loader: () => Item[] | Promise<Item[]>) => {
|
const handleLoadChildren = async (keyPath: string, loader: () => Item[] | Promise<Item[]>) => {
|
||||||
if (loadedChildren[keyPath] || loadingKeys.has(keyPath)) return;
|
if (loadedChildren[keyPath] || loadingKeys.has(keyPath)) return;
|
||||||
@ -131,6 +83,193 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 自动加载所有 group 的异步 children
|
||||||
|
useEffect(() => {
|
||||||
|
if (!menuVisible || !rootItems.length) return;
|
||||||
|
const asyncGroups = collectAsyncGroups(rootItems);
|
||||||
|
for (const [keyPath, loader] of asyncGroups) {
|
||||||
|
if (!loadedChildren[keyPath] && !loadingKeys.has(keyPath)) {
|
||||||
|
handleLoadChildren(keyPath, loader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [menuVisible, rootItems]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadedChildren,
|
||||||
|
loadingKeys,
|
||||||
|
handleLoadChildren,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理菜单搜索状态
|
||||||
|
*/
|
||||||
|
const useMenuSearch = () => {
|
||||||
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const updateSearchValue = (key: string, value: string) => {
|
||||||
|
setIsSearching(true);
|
||||||
|
setSearchValues((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchValues,
|
||||||
|
isSearching,
|
||||||
|
updateSearchValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理动态子菜单样式
|
||||||
|
*/
|
||||||
|
const useSubmenuStyles = (menuVisible: boolean, dropdownMaxHeight: number) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!menuVisible || dropdownMaxHeight <= 0) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as HTMLElement;
|
||||||
|
|
||||||
|
if (element.classList?.contains('ant-dropdown-menu-submenu-popup')) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const menuContainer = element.querySelector('.ant-dropdown-menu');
|
||||||
|
if (menuContainer) {
|
||||||
|
const container = menuContainer as HTMLElement;
|
||||||
|
container.style.maxHeight = `${dropdownMaxHeight}px`;
|
||||||
|
container.style.overflowY = 'auto';
|
||||||
|
container.classList.add('submenu-ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
const submenuPopups = document.querySelectorAll('.ant-dropdown-menu-submenu-popup .ant-dropdown-menu');
|
||||||
|
submenuPopups.forEach((menu) => {
|
||||||
|
const container = menu as HTMLElement;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.width > 0 && rect.height > 0 && !container.classList.contains('submenu-ready')) {
|
||||||
|
container.style.maxHeight = `${dropdownMaxHeight}px`;
|
||||||
|
container.style.overflowY = 'auto';
|
||||||
|
container.classList.add('submenu-ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [menuVisible, dropdownMaxHeight]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Components ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使搜索输入框在显示下拉菜单时自动聚焦
|
||||||
|
*/
|
||||||
|
const SearchInputWithAutoFocus: FC<InputProps & { visible: boolean }> = (props) => {
|
||||||
|
const inputRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && props.visible) {
|
||||||
|
inputRef.current.input.focus();
|
||||||
|
}
|
||||||
|
}, [props.visible]);
|
||||||
|
|
||||||
|
return <Input ref={inputRef} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Utility Functions ====================
|
||||||
|
|
||||||
|
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
|
||||||
|
|
||||||
|
const createSearchItem = (
|
||||||
|
item: Item,
|
||||||
|
searchKey: string,
|
||||||
|
currentSearchValue: string,
|
||||||
|
menuVisible: boolean,
|
||||||
|
t: (key: string) => string,
|
||||||
|
updateSearchValue: (key: string, value: string) => void,
|
||||||
|
) => ({
|
||||||
|
key: `${item.key}-search`,
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
<SearchInputWithAutoFocus
|
||||||
|
visible={menuVisible}
|
||||||
|
variant="borderless"
|
||||||
|
allowClear
|
||||||
|
placeholder={t(item.searchPlaceholder || 'Search')}
|
||||||
|
value={currentSearchValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateSearchValue(searchKey, e.target.value);
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createEmptyItem = (itemKey: string, t: (key: string) => string) => ({
|
||||||
|
key: `${itemKey}-empty`,
|
||||||
|
label: (
|
||||||
|
<div style={{ padding: '16px', textAlign: 'center' as const }}>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} style={{ margin: 0 }} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Main Component ====================
|
||||||
|
|
||||||
|
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
||||||
|
const model = useFlowModel();
|
||||||
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
|
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
||||||
|
const [rootItems, setRootItems] = useState<Item[]>([]);
|
||||||
|
const [rootLoading, setRootLoading] = useState(false);
|
||||||
|
const dropdownMaxHeight = useNiceDropdownMaxHeight();
|
||||||
|
const t = model.translate;
|
||||||
|
|
||||||
|
// 使用自定义 hooks
|
||||||
|
const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems(menuVisible, rootItems);
|
||||||
|
const { searchValues, isSearching, updateSearchValue } = useMenuSearch();
|
||||||
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
||||||
|
|
||||||
// 加载根 items,支持同步/异步函数
|
// 加载根 items,支持同步/异步函数
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRootItems = async () => {
|
const loadRootItems = async () => {
|
||||||
@ -154,17 +293,6 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
}
|
}
|
||||||
}, [menu.items, menuVisible]);
|
}, [menu.items, menuVisible]);
|
||||||
|
|
||||||
// 自动加载所有 group 的异步 children
|
|
||||||
useEffect(() => {
|
|
||||||
if (!menuVisible || !rootItems.length) return;
|
|
||||||
const asyncGroups = collectAsyncGroups(rootItems);
|
|
||||||
for (const [keyPath, loader] of asyncGroups) {
|
|
||||||
if (!loadedChildren[keyPath] && !loadingKeys.has(keyPath)) {
|
|
||||||
handleLoadChildren(keyPath, loader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [menuVisible, rootItems]);
|
|
||||||
|
|
||||||
// 递归解析 items,支持 children 为同步/异步函数
|
// 递归解析 items,支持 children 为同步/异步函数
|
||||||
const resolveItems = (items: Item[], path: string[] = []): any[] => {
|
const resolveItems = (items: Item[], path: string[] = []): any[] => {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
@ -217,61 +345,11 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
// 重新解析过滤后的 children
|
// 重新解析过滤后的 children
|
||||||
const resolvedFilteredChildren = resolveItems(filteredChildren, [...path, item.key]);
|
const resolvedFilteredChildren = resolveItems(filteredChildren, [...path, item.key]);
|
||||||
|
|
||||||
// 创建搜索框项
|
const searchItem = createSearchItem(item, searchKey, currentSearchValue, menuVisible, t, updateSearchValue);
|
||||||
const searchItem = {
|
const dividerItem = { key: `${item.key}-search-divider`, type: 'divider' as const };
|
||||||
key: `${item.key}-search`,
|
|
||||||
label: (
|
|
||||||
<div>
|
|
||||||
<SearchInputWithAutoFocus
|
|
||||||
visible={menuVisible}
|
|
||||||
variant="borderless"
|
|
||||||
allowClear
|
|
||||||
placeholder={t(item.searchPlaceholder || 'Search')}
|
|
||||||
value={currentSearchValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsSearching(true);
|
|
||||||
setSearchValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[searchKey]: e.target.value,
|
|
||||||
}));
|
|
||||||
// 清理之前的定时器
|
|
||||||
if (searchTimeoutRef.current) {
|
|
||||||
clearTimeout(searchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
// 搜索完成后重置搜索状态
|
|
||||||
searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300);
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
size="small"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
paddingLeft: 0,
|
|
||||||
paddingRight: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
disabled: true, // 搜索项不可点击
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建分割线项
|
|
||||||
const dividerItem = {
|
|
||||||
key: `${item.key}-search-divider`,
|
|
||||||
type: 'divider' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果搜索后没有结果,显示 Empty
|
|
||||||
if (currentSearchValue && resolvedFilteredChildren.length === 0) {
|
if (currentSearchValue && resolvedFilteredChildren.length === 0) {
|
||||||
const emptyItem = {
|
const emptyItem = createEmptyItem(item.key, t);
|
||||||
key: `${item.key}-empty`,
|
|
||||||
label: (
|
|
||||||
<div style={{ padding: '16px', textAlign: 'center' as const }}>
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} style={{ margin: 0 }} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
groupChildren = [searchItem, dividerItem, emptyItem];
|
groupChildren = [searchItem, dividerItem, emptyItem];
|
||||||
} else {
|
} else {
|
||||||
groupChildren = [searchItem, dividerItem, ...resolvedFilteredChildren];
|
groupChildren = [searchItem, dividerItem, ...resolvedFilteredChildren];
|
||||||
@ -293,14 +371,14 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
return {
|
return {
|
||||||
key: item.key,
|
key: item.key,
|
||||||
label: typeof item.label === 'string' ? t(item.label) : item.label,
|
label: typeof item.label === 'string' ? t(item.label) : item.label,
|
||||||
onClick: (info) => {
|
onClick: (info: any) => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
menu.onClick?.({
|
menu.onClick?.({
|
||||||
...info,
|
...info,
|
||||||
originalItem: item,
|
originalItem: item,
|
||||||
} as any); // 👈 强制扩展类型
|
} as any);
|
||||||
},
|
},
|
||||||
onMouseEnter: () => {
|
onMouseEnter: () => {
|
||||||
setOpenKeys((prev) => {
|
setOpenKeys((prev) => {
|
||||||
@ -314,57 +392,74 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|||||||
children && children.length > 0
|
children && children.length > 0
|
||||||
? resolveItems(children, [...path, item.key])
|
? resolveItems(children, [...path, item.key])
|
||||||
: children && children.length === 0
|
: children && children.length === 0
|
||||||
? [
|
? [createEmptyItem(keyPath, t)]
|
||||||
{
|
|
||||||
key: `${keyPath}-empty`,
|
|
||||||
label: (
|
|
||||||
<div style={{ padding: '16px', textAlign: 'center' as const }}>
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} style={{ margin: 0 }} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建优化的 CSS 样式
|
||||||
|
const overlayClassName = useMemo(() => {
|
||||||
|
return css`
|
||||||
|
.ant-dropdown-menu {
|
||||||
|
max-height: ${dropdownMaxHeight}px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子菜单初始状态:透明且准备好样式 */
|
||||||
|
.ant-dropdown-menu-submenu-popup .ant-dropdown-menu {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: ${dropdownMaxHeight}px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 样式设置完成后显示 */
|
||||||
|
.ant-dropdown-menu-submenu-popup .ant-dropdown-menu.submenu-ready {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对动态加载的深层菜单 */
|
||||||
|
.ant-dropdown-menu-submenu-popup .ant-dropdown-menu-submenu-popup .ant-dropdown-menu {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: ${dropdownMaxHeight}px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-submenu-popup .ant-dropdown-menu-submenu-popup .ant-dropdown-menu.submenu-ready {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}, [dropdownMaxHeight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
{...props}
|
{...props}
|
||||||
open={menuVisible}
|
open={menuVisible}
|
||||||
destroyPopupOnHide // 去掉的话会导致搜索框自动聚焦功能失效
|
destroyPopupOnHide
|
||||||
dropdownRender={() =>
|
overlayClassName={overlayClassName}
|
||||||
rootLoading && rootItems.length === 0 ? (
|
placement="bottomLeft"
|
||||||
<Menu
|
menu={{
|
||||||
items={[
|
...menu,
|
||||||
|
items:
|
||||||
|
rootLoading && rootItems.length === 0
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
key: `root-loading`,
|
key: 'root-loading',
|
||||||
label: <Spin size="small" />,
|
label: <Spin size="small" />,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]}
|
]
|
||||||
style={{
|
: resolveItems(rootItems),
|
||||||
maxHeight: dropdownMaxHeight,
|
onClick: () => {},
|
||||||
overflowY: 'auto',
|
style: {
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Menu
|
|
||||||
{...menu}
|
|
||||||
onClick={() => {}}
|
|
||||||
items={resolveItems(rootItems)}
|
|
||||||
style={{
|
|
||||||
maxHeight: dropdownMaxHeight,
|
maxHeight: dropdownMaxHeight,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
...menu?.style,
|
...menu?.style,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onOpenChange={(visible) => {
|
onOpenChange={(visible) => {
|
||||||
// 如果正在搜索且菜单要关闭,阻止关闭
|
|
||||||
if (!visible && isSearching) {
|
if (!visible && isSearching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user