mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 23:16:03 +00:00
perf(PageTabs): cache rendered content to prevent re-rendering
This commit is contained in:
parent
79472624b5
commit
f6de620be5
@ -14,7 +14,7 @@ import { FormLayout } from '@formily/antd-v5';
|
||||
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
||||
import { Button, Tabs } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavigateFunction, Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
|
||||
@ -42,7 +42,7 @@ export const Page = (props) => {
|
||||
const dn = useDesignable();
|
||||
const { theme } = useGlobalTheme();
|
||||
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
||||
const { tabUid, name: pageUid } = useParams();
|
||||
const { tabUid } = useParams();
|
||||
const basenameOfCurrentRouter = useRouterBasename();
|
||||
|
||||
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
||||
@ -63,105 +63,114 @@ export const Page = (props) => {
|
||||
console.error(error);
|
||||
}, []);
|
||||
|
||||
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
|
||||
const tabBarStyle = useMemo(
|
||||
() => ({
|
||||
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
|
||||
paddingRight: token.paddingLG - token.paddingPageHorizontal,
|
||||
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
||||
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
||||
}),
|
||||
[token.paddingLG, token.paddingPageHorizontal],
|
||||
);
|
||||
|
||||
const handleTabsChange = useCallback(
|
||||
(activeKey: string): void => {
|
||||
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
|
||||
},
|
||||
[basenameOfCurrentRouter, navigate],
|
||||
);
|
||||
|
||||
const tabBarExtraContent = useMemo(() => {
|
||||
return (
|
||||
dn.designable && (
|
||||
<Button
|
||||
aria-label={getAriaLabel('tabs')}
|
||||
icon={<PlusOutlined />}
|
||||
className={'addTabBtn'}
|
||||
type={'dashed'}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog(
|
||||
t('Add tab'),
|
||||
() => {
|
||||
return (
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Tab name'),
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
},
|
||||
theme,
|
||||
).open({
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, icon } = values;
|
||||
dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
title,
|
||||
'x-icon': icon,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Add tab')}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}, [dn, getAriaLabel, options?.components, options?.scope, t, theme]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return fieldSchema.mapProperties((schema) => {
|
||||
return {
|
||||
label: (
|
||||
<SortableItem
|
||||
id={schema.name as string}
|
||||
schema={schema}
|
||||
className={classNames('nb-action-link', 'designerCss', props.className)}
|
||||
>
|
||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||
<span>{schema.title || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
</SortableItem>
|
||||
),
|
||||
key: schema.name as string,
|
||||
};
|
||||
});
|
||||
}, [fieldSchema, props.className, t]);
|
||||
|
||||
const footer = useMemo(() => {
|
||||
return enablePageTabs ? (
|
||||
<DndContext>
|
||||
<Tabs
|
||||
size={'small'}
|
||||
activeKey={activeKey}
|
||||
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
|
||||
tabBarStyle={{
|
||||
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
|
||||
paddingRight: token.paddingLG - token.paddingPageHorizontal,
|
||||
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
||||
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
||||
}}
|
||||
onChange={(activeKey) => {
|
||||
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
|
||||
}}
|
||||
tabBarExtraContent={
|
||||
dn.designable && (
|
||||
<Button
|
||||
aria-label={getAriaLabel('tabs')}
|
||||
icon={<PlusOutlined />}
|
||||
className={'addTabBtn'}
|
||||
type={'dashed'}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog(
|
||||
t('Add tab'),
|
||||
() => {
|
||||
return (
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Tab name'),
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
},
|
||||
theme,
|
||||
).open({
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, icon } = values;
|
||||
dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
title,
|
||||
'x-icon': icon,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Add tab')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
items={fieldSchema.mapProperties((schema) => {
|
||||
return {
|
||||
label: (
|
||||
<SortableItem
|
||||
id={schema.name as string}
|
||||
schema={schema}
|
||||
className={classNames('nb-action-link', 'designerCss', props.className)}
|
||||
>
|
||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||
<span>{schema.title || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
</SortableItem>
|
||||
),
|
||||
key: schema.name as string,
|
||||
};
|
||||
})}
|
||||
tabBarStyle={tabBarStyle}
|
||||
onChange={handleTabsChange}
|
||||
tabBarExtraContent={tabBarExtraContent}
|
||||
items={items}
|
||||
/>
|
||||
</DndContext>
|
||||
) : null;
|
||||
}, [
|
||||
activeKey,
|
||||
fieldSchema,
|
||||
dn.designable,
|
||||
options.scope,
|
||||
options.components,
|
||||
pageUid,
|
||||
fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(),
|
||||
enablePageTabs,
|
||||
]);
|
||||
}, [activeKey, enablePageTabs, handleTabsChange, items, tabBarExtraContent, tabBarStyle]);
|
||||
|
||||
return wrapSSR(
|
||||
<div className={`${componentCls} ${hashId} ${antTableCell}`}>
|
||||
@ -217,6 +226,35 @@ const className1 = css`
|
||||
}
|
||||
`;
|
||||
|
||||
// Add a TabPane component to manage caching, implementing an effect similar to Vue's keep-alive
|
||||
const TabPane = React.memo(({ schema, active }: { schema: Schema; active: boolean }) => {
|
||||
const mountedRef = useRef(false);
|
||||
|
||||
if (active && !mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
}
|
||||
|
||||
const newSchema = useMemo(
|
||||
() =>
|
||||
new Schema({
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
}),
|
||||
[schema],
|
||||
);
|
||||
|
||||
if (!mountedRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: active ? 'block' : 'none' }}>
|
||||
<SchemaComponent distributed schema={newSchema} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const PageContent = memo(
|
||||
({
|
||||
loading,
|
||||
@ -231,44 +269,23 @@ const PageContent = memo(
|
||||
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
|
||||
activeKey: string;
|
||||
}) => {
|
||||
// const { render } = useAppSpin();
|
||||
|
||||
// if (loading) {
|
||||
// return render();
|
||||
// }g
|
||||
|
||||
if (!disablePageHeader && enablePageTabs) {
|
||||
const result = fieldSchema
|
||||
.mapProperties((schema) => {
|
||||
if (schema.name !== activeKey) return null;
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
key={schema.name}
|
||||
distributed
|
||||
schema={
|
||||
new Schema({
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return result[0];
|
||||
} else {
|
||||
return (
|
||||
<div className={className1}>
|
||||
<SchemaComponent schema={fieldSchema} distributed />
|
||||
</div>
|
||||
<>
|
||||
{fieldSchema.mapProperties((schema) => (
|
||||
<TabPane key={schema.name} schema={schema} active={schema.name === activeKey} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className1}>
|
||||
<SchemaComponent schema={fieldSchema} distributed />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
PageContent.displayName = 'PageContent';
|
||||
|
||||
function NocoBasePageHeader({ footer }: { footer: React.JSX.Element }) {
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
Loading…
Reference in New Issue
Block a user