perf: multiple menu

This commit is contained in:
archer 2025-06-03 22:53:42 +08:00
parent c1f8d5b032
commit 98b00ae86d
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
7 changed files with 103 additions and 117 deletions

View File

@ -28,6 +28,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4911' \
1. 商业版支持图片知识库。 1. 商业版支持图片知识库。
2. 工作流中增加节点搜索功能。 2. 工作流中增加节点搜索功能。
3. 工作流中,子流程版本控制,可选择“保持最新版本”,无需手动更新。 3. 工作流中,子流程版本控制,可选择“保持最新版本”,无需手动更新。
4. 增加更多审计操作日志。
## ⚙️ 优化 ## ⚙️ 优化

View File

@ -17,6 +17,7 @@ export const ApiDatasetTypeMap: Record<
`${DatasetTypeEnum}`, `${DatasetTypeEnum}`,
{ {
icon: string; icon: string;
avatar: string;
label: any; label: any;
collectionLabel: string; collectionLabel: string;
courseUrl?: string; courseUrl?: string;
@ -24,18 +25,21 @@ export const ApiDatasetTypeMap: Record<
> = { > = {
[DatasetTypeEnum.apiDataset]: { [DatasetTypeEnum.apiDataset]: {
icon: 'core/dataset/externalDatasetOutline', icon: 'core/dataset/externalDatasetOutline',
avatar: 'core/dataset/externalDatasetColor',
label: i18nT('dataset:api_file'), label: i18nT('dataset:api_file'),
collectionLabel: i18nT('common:File'), collectionLabel: i18nT('common:File'),
courseUrl: '/docs/guide/knowledge_base/api_dataset/' courseUrl: '/docs/guide/knowledge_base/api_dataset/'
}, },
[DatasetTypeEnum.feishu]: { [DatasetTypeEnum.feishu]: {
icon: 'core/dataset/feishuDatasetOutline', icon: 'core/dataset/feishuDatasetOutline',
avatar: 'core/dataset/feishuDatasetColor',
label: i18nT('dataset:feishu_dataset'), label: i18nT('dataset:feishu_dataset'),
collectionLabel: i18nT('common:File'), collectionLabel: i18nT('common:File'),
courseUrl: '/docs/guide/knowledge_base/lark_dataset/' courseUrl: '/docs/guide/knowledge_base/lark_dataset/'
}, },
[DatasetTypeEnum.yuque]: { [DatasetTypeEnum.yuque]: {
icon: 'core/dataset/yuqueDatasetOutline', icon: 'core/dataset/yuqueDatasetOutline',
avatar: 'core/dataset/yuqueDatasetColor',
label: i18nT('dataset:yuque_dataset'), label: i18nT('dataset:yuque_dataset'),
collectionLabel: i18nT('common:File'), collectionLabel: i18nT('common:File'),
courseUrl: '/docs/guide/knowledge_base/yuque_dataset/' courseUrl: '/docs/guide/knowledge_base/yuque_dataset/'
@ -45,6 +49,7 @@ export const DatasetTypeMap: Record<
`${DatasetTypeEnum}`, `${DatasetTypeEnum}`,
{ {
icon: string; icon: string;
avatar: string;
label: any; label: any;
collectionLabel: string; collectionLabel: string;
courseUrl?: string; courseUrl?: string;
@ -53,22 +58,26 @@ export const DatasetTypeMap: Record<
...ApiDatasetTypeMap, ...ApiDatasetTypeMap,
[DatasetTypeEnum.folder]: { [DatasetTypeEnum.folder]: {
icon: 'common/folderFill', icon: 'common/folderFill',
avatar: 'common/folderFill',
label: i18nT('dataset:folder_dataset'), label: i18nT('dataset:folder_dataset'),
collectionLabel: i18nT('common:Folder') collectionLabel: i18nT('common:Folder')
}, },
[DatasetTypeEnum.dataset]: { [DatasetTypeEnum.dataset]: {
icon: 'core/dataset/commonDatasetOutline', icon: 'core/dataset/commonDatasetOutline',
avatar: 'core/dataset/commonDatasetColor',
label: i18nT('dataset:common_dataset'), label: i18nT('dataset:common_dataset'),
collectionLabel: i18nT('common:File') collectionLabel: i18nT('common:File')
}, },
[DatasetTypeEnum.websiteDataset]: { [DatasetTypeEnum.websiteDataset]: {
icon: 'core/dataset/websiteDatasetOutline', icon: 'core/dataset/websiteDatasetOutline',
avatar: 'core/dataset/websiteDatasetColor',
label: i18nT('dataset:website_dataset'), label: i18nT('dataset:website_dataset'),
collectionLabel: i18nT('common:Website'), collectionLabel: i18nT('common:Website'),
courseUrl: '/docs/guide/knowledge_base/websync/' courseUrl: '/docs/guide/knowledge_base/websync/'
}, },
[DatasetTypeEnum.externalFile]: { [DatasetTypeEnum.externalFile]: {
icon: 'core/dataset/externalDatasetOutline', icon: 'core/dataset/externalDatasetOutline',
avatar: 'core/dataset/externalDatasetColor',
label: i18nT('dataset:external_file'), label: i18nT('dataset:external_file'),
collectionLabel: i18nT('common:File') collectionLabel: i18nT('common:File')
} }

View File

@ -36,10 +36,12 @@ export type Props = {
label?: string; label?: string;
width?: number | string; width?: number | string;
offset?: [number, number]; offset?: [number, number];
Button: React.ReactNode; Trigger: React.ReactNode;
trigger?: 'hover' | 'click'; trigger?: 'hover' | 'click';
size?: MenuSizeType; size?: MenuSizeType;
placement?: PlacementWithLogical; placement?: PlacementWithLogical;
hasArrow?: boolean;
onClose?: () => void;
menuList: MenuItemData[]; menuList: MenuItemData[];
}; };
@ -189,32 +191,32 @@ const sizeMapStyle: Record<
} }
}; };
const MenuItem = React.forwardRef< const MenuItem = ({
HTMLDivElement, item,
{ size,
item: MenuItemData['children'][number]; onClose
size: MenuSizeType; }: {
onClose: () => void; item: MenuItemData['children'][number];
} size: MenuSizeType;
>((props, ref) => { onClose: () => void;
const { item, size, onClose } = props; }) => {
return ( return (
<Box <Box
ref={ref}
px={3} px={3}
py={2} py={2}
cursor="pointer" cursor="pointer"
borderRadius="md"
_hover={{ _hover={{
bg: 'primary.50', bg: 'primary.50',
color: 'primary.600' color: 'primary.600'
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); if (item.onClick) {
if (item.onClick && !item.menuList) {
onClose();
item.onClick(); item.onClick();
} }
if (!item.menuList) {
onClose();
}
}} }}
> >
<Flex alignItems="center" w="100%"> <Flex alignItems="center" w="100%">
@ -242,41 +244,53 @@ const MenuItem = React.forwardRef<
</Flex> </Flex>
</Box> </Box>
); );
}); };
MenuItem.displayName = 'MenuItem'; const MultipleMenu = (props: Props) => {
const {
width = 'auto',
trigger = 'hover',
size = 'sm',
offset,
Trigger,
menuList,
hasArrow = false,
placement = 'bottom-start'
} = props;
const MyMenu = ({
width = 'auto',
trigger = 'hover',
size = 'sm',
offset,
Button,
menuList,
placement = 'bottom-start'
}: Props) => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const ref = useRef<HTMLDivElement>(null);
const formatTrigger = !isPc ? 'click' : trigger; const formatTrigger = !isPc ? 'click' : trigger;
return ( return (
<Box ref={ref} display="inline-block"> <MyPopover
<MyPopover placement={placement}
placement={placement} offset={offset}
offset={offset || [0, 5]} hasArrow={hasArrow}
hasArrow trigger={formatTrigger}
trigger={formatTrigger} w={width}
w={width} zIndex={999}
zIndex={999} closeOnBlur={false}
closeOnBlur={false} autoFocus={false}
autoFocus={false} Trigger={Trigger}
Trigger={Button} >
> {({ onClose }) => {
{({ onClose }) => ( const onCloseFn = () => {
<Box bg="white" minW="150px" maxW="300px" p="6px" borderRadius="md" boxShadow="md"> onClose();
props?.onClose?.();
};
return (
<Box
bg="white"
maxW="300px"
p="6px"
border={'1px solid #fff'}
boxShadow={'3'}
borderRadius={'md'}
>
{menuList.map((group, i) => ( {menuList.map((group, i) => (
<Box key={i}> <Box key={i}>
{i !== 0 && <MyDivider {...sizeMapStyle[size].dividerStyle} />} {i !== 0 && <MyDivider h={'1.5px'} {...sizeMapStyle[size].dividerStyle} />}
{group.label && ( {group.label && (
<Box fontSize="sm" px={3} py={1} color="myGray.500"> <Box fontSize="sm" px={3} py={1} color="myGray.500">
{group.label} {group.label}
@ -286,54 +300,21 @@ const MyMenu = ({
return ( return (
<Box key={index}> <Box key={index}>
{item.menuList ? ( {item.menuList ? (
<MyPopover <MultipleMenu
placement="right-start" {...props}
offset={[10, 10]} placement={'left'}
hasArrow trigger={'hover'}
trigger={formatTrigger} menuList={item.menuList}
w={'auto'} onClose={onCloseFn}
zIndex={1000}
closeOnBlur={false}
autoFocus={false}
Trigger={ Trigger={
<Box position="relative"> <Box>
<MenuItem item={item} size={size} onClose={onClose} /> <MenuItem item={item} size={size} onClose={onCloseFn} />
</Box> </Box>
} }
> hasArrow
{({ onClose: onCloseSubmenu }) => { />
return (
<Box
bg="white"
minW="150px"
maxW="300px"
p="6px"
borderRadius="md"
boxShadow="md"
position="relative"
zIndex={1001}
>
{item.menuList?.map((subGroup, subI) => (
<Box key={subI}>
{subGroup.children.map((subItem, subIndex) => (
<MenuItem
key={subIndex}
item={subItem}
size={size}
onClose={() => {
onClose();
onCloseSubmenu();
}}
/>
))}
</Box>
))}
</Box>
);
}}
</MyPopover>
) : ( ) : (
<MenuItem item={item} size={size} onClose={onClose} /> <MenuItem item={item} size={size} onClose={onCloseFn} />
)} )}
</Box> </Box>
); );
@ -341,10 +322,10 @@ const MyMenu = ({
</Box> </Box>
))} ))}
</Box> </Box>
)} );
</MyPopover> }}
</Box> </MyPopover>
); );
}; };
export default React.memo(MyMenu); export default React.memo(MultipleMenu);

View File

@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { import {
Menu, Menu,
MenuList, MenuList,
@ -18,9 +18,20 @@ import { useSystem } from '../../../hooks/useSystem';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg'; export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg';
export type MenuSizeType = 'sm' | 'md' | 'xs' | 'mini'; export type MenuSizeType = 'sm' | 'md' | 'xs' | 'mini';
export type MenuItemData = {
label?: string;
children: Array<{
isActive?: boolean;
type?: MenuItemType;
icon?: IconNameType | string;
label: string | React.ReactNode;
description?: string;
onClick?: () => any;
menuItemStyles?: MenuItemProps;
}>;
};
export type Props = { export type Props = {
width?: number | string; width?: number | string;
offset?: [number, number]; offset?: [number, number];
@ -29,18 +40,7 @@ export type Props = {
size?: MenuSizeType; size?: MenuSizeType;
placement?: PlacementWithLogical; placement?: PlacementWithLogical;
menuList: { menuList: MenuItemData[];
label?: string;
children: {
isActive?: boolean;
type?: MenuItemType;
icon?: IconNameType | string;
label: string | React.ReactNode;
description?: string;
onClick?: () => any;
menuItemStyles?: MenuItemProps;
}[];
}[];
}; };
const typeMapStyle: Record<MenuItemType, { styles: MenuItemProps; iconColor?: string }> = { const typeMapStyle: Record<MenuItemType, { styles: MenuItemProps; iconColor?: string }> = {

View File

@ -43,11 +43,11 @@ const MyPopover = ({
initialFocusRef={firstFieldRef} initialFocusRef={firstFieldRef}
onOpen={() => { onOpen={() => {
onOpen(); onOpen();
onOpenFunc && onOpenFunc(); onOpenFunc?.();
}} }}
onClose={() => { onClose={() => {
onClose(); onClose();
onCloseFunc && onCloseFunc(); onCloseFunc?.();
}} }}
placement={placement} placement={placement}
offset={offset} offset={offset}

View File

@ -50,7 +50,7 @@ const CreateModal = ({
defaultValues: { defaultValues: {
parentId, parentId,
type: type || DatasetTypeEnum.dataset, type: type || DatasetTypeEnum.dataset,
avatar: DatasetTypeMap[type].icon, avatar: DatasetTypeMap[type].avatar,
name: '', name: '',
intro: '', intro: '',
vectorModel: vectorModel:
@ -95,7 +95,7 @@ const CreateModal = ({
w={'20px'} w={'20px'}
h={'20px'} h={'20px'}
borderRadius={'xs'} borderRadius={'xs'}
src={DatasetTypeMap[type].icon} src={DatasetTypeMap[type].avatar}
pr={'10px'} pr={'10px'}
/> />
{t('common:core.dataset.Create dataset', { name: t(DatasetTypeMap[type].label) })} {t('common:core.dataset.Create dataset', { name: t(DatasetTypeMap[type].label) })}

View File

@ -9,7 +9,7 @@ import List from '@/pageComponents/dataset/list/List';
import { DatasetsContext } from './context'; import { DatasetsContext } from './context';
import DatasetContextProvider from './context'; import DatasetContextProvider from './context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import MyMenu from '@fastgpt/web/components/common/MyMenu/secondary'; import MultipleMenu from '@fastgpt/web/components/common/MyMenu/Multiple';
import { AddIcon } from '@chakra-ui/icons'; import { AddIcon } from '@chakra-ui/icons';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@ -138,10 +138,9 @@ const Dataset = () => {
? folderDetail.permission.hasWritePer ? folderDetail.permission.hasWritePer
: userInfo?.team?.permission.hasDatasetCreatePer) && ( : userInfo?.team?.permission.hasDatasetCreatePer) && (
<Box pl={[0, 4]}> <Box pl={[0, 4]}>
<MyMenu <MultipleMenu
size="md" size="md"
offset={[0, 10]} Trigger={
Button={
<Button variant={'primary'} px="0"> <Button variant={'primary'} px="0">
<Flex alignItems={'center'} px={5}> <Flex alignItems={'center'} px={5}>
<AddIcon mr={2} /> <AddIcon mr={2} />
@ -163,11 +162,7 @@ const Dataset = () => {
label: t('dataset:website_dataset'), label: t('dataset:website_dataset'),
description: t('dataset:website_dataset_desc'), description: t('dataset:website_dataset_desc'),
onClick: () => onSelectDatasetType(DatasetTypeEnum.websiteDataset) onClick: () => onSelectDatasetType(DatasetTypeEnum.websiteDataset)
} },
]
},
{
children: [
{ {
icon: 'core/dataset/otherDataset', icon: 'core/dataset/otherDataset',
label: t('dataset:other_dataset'), label: t('dataset:other_dataset'),